Web SDK - 原生 JavaScript 实现指南

文件

概述

INFO:Web归因是企业级功能。请联系您的客户成功经理为您的账户启用此功能。

本指南将指导您使用原生 JavaScript 实现 Singular WebSDK。此方法提供最可靠的跟踪效果,且不受常见广告拦截器阻挡,因此推荐用于大多数实施场景。

重要提示!

  • 请勿同时采用原生 JavaScript 和 Google Tag Manager 两种方法,仅选择其中一种以避免重复追踪。
  • Singular WebSDK专为用户浏览器端运行设计。其正常运作需访问浏览器功能 如localStorage和 文档对象模型DOM)。请勿尝试在服务器端运行SDK(例如通过Next.js SSR或node.js),因服务器环境无法访问浏览器API,将导致追踪失败。

前提条件

开始前请确保您拥有:

  • SDK密钥与SDK密钥密钥: 
  • 产品ID:
    • 定义:网站的唯一标识名,建议采用反向DNS格式(例如:com.website-name )。
    • 重要性说明:该ID将网站关联为Singular中的应用程序,必须与您在Singular应用页面中列出的Web应用程序bundleID完全一致。 
  • 编辑网站HTML代码的权限。
  • 在页面<head> 区域添加JavaScript的权限。
  • 您希望追踪的事件列表。参考我们的《Singular标准事件:完整列表》及《垂直领域推荐事件》获取灵感。

实施步骤


步骤1:添加SDK库脚本

将以下代码片段添加至网站所有页面的<head> 部分。请尽可能置于页面顶部,理想位置为<head> 标签附近。

提示!提前添加脚本可确保页面源代码中的所有Singular函数都能调用JavaScript库。

Latest VersionSpecific VersionUsing NPMNext.js / React
<script src="https://web-sdk-cdn.singular.net/singular-sdk/latest/singular-sdk.js"></script>

步骤 2:初始化 SDK

  • 每次页面在浏览器加载时都需初始化 SDK。
  • 所有Singular归因和事件追踪功能均需初始化。
  • 初始化将触发 __PAGE_VISIT__ 事件
  • 当满足以下条件时, __PAGE_VISIT__ 事件用于在满足以下条件时在服务器端生成新会话
    • 用户携带新广告数据(如UTM或WP参数)通过URL访问,或
    • 前次会话已过期(30分钟无操作后)。
    • 会话用于衡量用户留存率并支持重新参与归因分析。
  1. 创建初始化函数,并在页面加载完成后的DOM就绪阶段调用该函数。
  2. 确保在报告任何其他 Singular 事件之前完成初始化。
  3. 对于单页应用程序(SPA),请在首次页面加载时初始化Singular SDK,并在每次路由变更(代表新页面浏览)调用Singular页面访问函数window.singularSdk.pageVisit()
Basic InitializationNext.js / React

基础DOM就绪初始化

DOMContentLoaded页面上添加事件监听器,用于调用initSingularSDK()函数

/**
 * Initializes the Singular SDK with the provided configuration.
 * @param {string} sdkKey - The SDK key for Singular.
 * @param {string} sdkSecret - The SDK secret for Singular.
 * @param {string} productId - The product ID for Singular.
 * @example
 * initSingularSDK(); // Initializes SDK with default config
 */
function initSingularSDK() {
  var config = new SingularConfig('sdkKey', 'sdkSecret', 'productId');
  window.singularSdk.init(config);
}

/**
 * Triggers Singular SDK initialization when the DOM is fully parsed.
 */
document.addEventListener('DOMContentLoaded', function() {
  initSingularSDK();
});

全局属性基础初始化

由于Singular SDK尚未初始化,您必须在浏览器的localstorage 中 实现自定义函数来设置全局属性。 请参阅自定义函数setGlobalPropertyBeforeInit() 。 实现后,您可在SDK初始化前按以下方式设置属性:

/**
 * Initializes the Singular SDK with the provided configuration.
 * @param {string} sdkKey - The SDK key for Singular.
 * @param {string} sdkSecret - The SDK secret for Singular.
 * @param {string} productId - The product ID for Singular.
 * @example
 * initSingularSDK(); // Initializes SDK with default config
 */
function initSingularSDK() {
  var sdkKey = 'sdkKey';
  var sdkSecret = 'sdkSecret';
  var productId = 'productId';
  
  // Set global properties before SDK initialization
  setGlobalPropertyBeforeInit(sdkKey, productId, 'global_prop_1', 'test', false);
  setGlobalPropertyBeforeInit(sdkKey, productId, 'global_prop_2', 'US', true);
  
  // Initialize SDK
  var config = new SingularConfig('sdkKey', 'sdkSecret', 'productId');
  window.singularSdk.init(config);
}

/**
 * Triggers Singular SDK initialization when the DOM is fully parsed.
 */
document.addEventListener('DOMContentLoaded', function() {
  initSingularSDK();
});

基于单页应用(SPA)路由的基本初始化

场景 操作步骤

首次页面加载

调用window.singularSdk.init(config)

导航至新路由/页面

调用window.singularSdk.pageVisit()

在SPA初始加载时

不调用window.singularSdk.pageVisit() (初始化将提供首次页面访问事件)

SPA示例(React)

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

/**
 * Initializes the Singular SDK with the provided configuration.
 * @param {string} sdkKey - The SDK key for Singular.
 * @param {string} sdkSecret - The SDK secret for Singular.
 * @param {string} productId - The product ID for Singular.
 * @example
 * // Initialize the Singular SDK
 * initSingularSDK();
 */
function initSingularSDK() {
  var config = new SingularConfig('sdkKey', 'sdkSecret', 'productId');
  window.singularSdk.init(config);
}

/**
 * Tracks a page visit event with the Singular SDK on route changes.
 * @example
 * // Track a page visit
 * trackPageVisit();
 */
function trackPageVisit() {
  window.singularSdk.pageVisit();
}

/**
 * A React component that initializes the Singular SDK on mount and tracks page visits on route changes.
 * @returns {JSX.Element} The component rendering the SPA content.
 * @example
 * // Use in a React SPA with react-router-dom
 * function App() {
 *   const location = useLocation();
 *   useEffect(() => {
 *     initSingularSDK();
 *   }, []);
 *   useEffect(() => {
 *     trackPageVisit();
 *   }, [location.pathname]);
 *   return <div>Your SPA Content</div>;
 * }
 */
function App() {
  const location = useLocation();

  useEffect(() => {
    initSingularSDK();
  }, []); // Run once on mount for SDK initialization

  useEffect(() => {
    trackPageVisit();
  }, [location.pathname]); // Run on route changes for page visits

  return (
    <div>Your SPA Content</div>
  );
}

export default App;

基础初始化使用初始化回调

若需在SDK就绪后执行代码(例如获取 Singular 设备ID),请通过.withInitFinishedCallback() 设置回调:

/**
 * Initializes the Singular SDK with a callback to handle initialization completion.
 * @param {string} sdkKey - The SDK key for Singular.
 * @param {string} sdkSecret - The SDK secret for Singular.
 * @param {string} productId - The product ID for Singular.
 * @example
 * initSingularSDK(); // Initializes SDK and logs device ID
 */
function initSingularSDK() {
  var config = new SingularConfig('sdkKey', 'sdkSecret', 'productId')
    .withInitFinishedCallback(function(initParams) {
      var singularDeviceId = initParams.singularDeviceId;
      // Example: Store device ID for analytics
      console.log('Singular Device ID:', singularDeviceId);
      // Optionally store in localStorage or use for event tracking
      // localStorage.setItem('singularDeviceId', singularDeviceId);
    });

  window.singularSdk.init(config);
}

/**
 * Triggers Singular SDK initialization when the DOM is fully parsed.
 */
document.addEventListener('DOMContentLoaded', function() {
  initSingularSDK();
});
  • 'sdkKey' 替换为您的实际SDK密钥。
  • 'sdkSecret' 替换为您的实际SDK密钥。
  • 'productId' 替换为您的实际产品ID。格式应为:com.website-name ,且需与Singular平台应用页面中的BundleID值一致。

配置选项

通过链式调用.with 方法增强 WebSDK 配置以启用额外功能。

例如:通过在Cookie中持久化Singular设备ID(SDID)实现跨子域跟踪,或为处于活跃登录状态的回访用户添加自定义用户ID:

Config with Options
var domain = 'website-name.com';
var config = new SingularConfig('sdkKey','sdkSecret','productId')
  .withAutoPersistentSingularDeviceId(domain)
  .withCustomUserId(userId);

SingularConfig方法参考

以下是所有可用的".with"方法。

方法 描述 了解更多
.withCustomUserId(customId) 向 Singular 发送用户 ID 设置用户ID
.withProductName(productName) 产品的可选显示名称  
.withLogLevel(logLevel) 配置日志级别:0 - 无(默认);1 - 警告;2 - 信息;3 - 调试。  
.withSessionTimeoutInMinutes(timeout) 设置会话超时时间(单位:分钟,默认:30分钟)

 

.withAutoPersistentSingularDeviceId(domain) 启用跨子域自动跟踪 使用Cookie自动持久化

.withPersistentSingularDeviceId(singularDeviceId)

启用手动跨子域跟踪 手动设置唯一设备ID
.withInitFinishedCallback(callback) SDK初始化完成时调用回调函数 初始化完成时调用回调函数
.withGlobalProperties(globalProperties, false) 初始化期间设置全局属性;第二个参数 为overrideExisting (布尔值)。 使用false 与现有属性合并, 使用true 覆盖现有属性。 全局属性
.withEventsDedupEnabled() 启用可选事件去重功能以减少重复事件导出 (例如短时间内触发的重复事件) 事件去重
.withTimeBetweenEvents(timeBetweenEvents) 设置去重操作的最大时间窗口(毫秒,默认: 1000毫秒 / 1秒) 事件去重

开发者检查清单

  • 收集您的SDK凭证和产品ID。
  • 确定是否需要自定义设置(用户ID、超时等)。
  • 参照上述示例构建Singular初始化函数及SingularConfig对象。
  • 务必测试确保初始化仅在页面加载时触发一次。

提示!对于 自然搜索 追踪等高级设置,您可能需要在 Singular SDK 初始化前 实现自定义 JavaScript(例如调整查询参数)。 请确保自定义代码在 Singular 初始化函数前执行, 以正确捕获变更。自然搜索追踪的具体实现方法 详见此处


步骤3:事件追踪

初始化 SDK 后,当用户在网站执行关键操作时即可追踪自定义事件。

重要提示!Singular不会拦截重复事件!开发者需自行添加针对页面刷新或重复事件的防护机制。建议针对收入事件采用特定去重方法,以避免收入数据出现错误。具体示例请参见下文"步骤5:防止重复事件"。

Basic EventConversion EventRevenue Event

基础事件追踪

通过有效 JSON 格式追踪基础事件或添加自定义属性, 以提供更丰富的事件上下文:

// Basic event tracking
var eventName = 'page_view';
window.singularSdk.event(eventName);
      
// Optional: With attributes for more context
var eventName = 'sng_content_view';
var attributes = {
  key1: 'value1', // First custom attribute
  key2: 'value2'  // Second custom attribute
};
window.singularSdk.event(eventName, attributes);

常见事件实现模式

Page Load EventsButton Click EventsForm Submission Events

页面加载事件

页面加载事件追踪机制通过JavaScript监测网页完全加载状态, 随后使用Singular SDK触发分析事件。 具体而言,该机制利用window.addEventListener('load', ...) 方法 检测所有页面资源(如HTML、图片、脚本)的加载完成状态。 触发该事件时,将调用Singular SDK的event 函数 记录关联自定义属性的事件,从而实现用户交互追踪 (如监控页面浏览量或注册等用户行为)。该机制常用于 网络分析中捕获用户行为数据(如页面访问或特定操作), 并支持自定义属性以获取深度洞察。

该机制可适配追踪特定事件(如page_view 事件)及其自定义属性,如下所示。代码结构保持简洁模块化,事件名称与属性分别定义以提升清晰度和可维护性。

/**
 * Tracks a registration event with the Singular SDK when the page fully loads.
 * @param {string} eventName - The name of the event to track (e.g., 'page_view').
 * @param {Object} attributes - A JSON object with custom event attributes.
 * @param {string} attributes.key1 - First custom attribute (e.g., 'value1').
 * @param {string} attributes.key2 - Second custom attribute (e.g., 'value2').
 */
window.addEventListener('load', function() {
  var eventName = 'page_view';
  var attributes = {
    key1: 'value1', // First custom attribute
    key2: 'value2'  // Second custom attribute
  };
  window.singularSdk.event(eventName, attributes);
});

步骤4:设置客户用户ID

您可通过Singular SDK方法向平台发送内部用户ID。

注意:使用Singular跨设备解决方案,必须在所有平台收集用户ID。

  • 用户ID可采用任意标识符, 但不得暴露PII(个人身份信息)。 例如,请勿使用用户的电子邮箱、用户名或 电话号码。 Singular建议使用仅存在于您自有数据中的 哈希值。
  • 传递至Singular的用户ID值应与您在所有平台(网页/移动端/PC/主机/离线)捕获的内部用户ID保持一致。
  • Singular将在用户级导出、ETL及内部BI回传(若已配置)中包含用户ID。该ID属于第一方数据,Singular不会与第三方共享。
  • 通过Singular SDK方法设置的用户ID值将持续有效,直至: 通过logout() 方法取消设置 或 浏览器本地存储被清除。关闭或刷新网站不会取消用户ID设置。
  • 在隐私/无痕模式下,SDK无法持久化用户ID,因为浏览器关闭时会自动清除本地存储。

设置用户ID时请使用login() 方法。 若需清除用户ID(例如用户"退出"账户时),请调用logout() 方法。

注意:若多个用户共用单一设备,建议实现注销流程,在每次登录/注销时设置 /清除用户ID。

若在网站初始化Singular SDK时已知用户ID, 请在配置对象中设置用户ID。 这样Singular可在首次会话获取用户ID。 但通常用户ID需待 用户注册或登录后,请在注册流程完成后调用 login()

提示!请使用与移动端SDK相同的客户用户ID。这将实现跨设备归因,并提供用户跨平台行为的全景视图。

客户用户ID最佳实践:

  • 用户登录或注册时立即设置
  • 在网页和移动平台统一使用同一ID
  • 避免使用个人身份信息(邮箱、电话号码)
  • 使用内部用户ID或数据库ID
  • 即使为数字也应以字符串形式发送:
Set the User IDUnset the User ID
// After user logs in or signs up
var userId = 'user_12345';
window.singularSdk.login(userId);

步骤5:事件去重(可选)

重要提示!若您的网站在短时间内可能多次触发相同事件,导出数据中可能出现重复事件。启用SDK去重功能可自动抑制重复事件。

Singular的Web SDK支持可选事件去重功能,可减少因短时间内重复触发导致的重复导出。启用后,SDK将在配置的时间窗口内剔除匹配相同去重参数的重复事件。

启用方法

  • withEventsDedupEnabled :启用事件去重(默认禁用)。
  • withTimeBetweenEvents: 判定事件重复的最大时间窗口(毫秒制);默认值为1000毫秒(1秒)。
JavaScript

JavaScript

// Enable optional event deduplication (recommended when triggers may fire repeatedly)
var config = new SingularConfig("SDK_KEY", "SDK_SECRET", "PRODUCT_ID")
  .withEventsDedupEnabled()     // turns deduplication on
  .withTimeBetweenEvents(1000); // dedup window in ms (default: 1000 = 1s)

window.singularSdk.init(config);

重复事件检测机制

启用去重后,SDK将根据事件的关键字段计算哈希值,并在时间窗口内抑制重复事件。去重参数包括:EventNameEventProductNameIsRevenueEventCustomUserIdGlobalPropertiesMatchIdWebUrl (SDK在哈希计算时还会考虑额外事件数据,如收入/参数)。


步骤 6:测试实现

完成SDK集成后,请通过浏览器开发者工具验证其运行状态。

验证SDK加载状态

  1. 在浏览器中打开您的网站
  2. 打开开发者工具(F12 或右键点击 → 检查)
  3. 切换至控制台选项卡
  4. 输入typeof singularSdk 并按Enter键
  5. 应显示"function"对象而非"undefined"

验证网络请求

  1. 打开开发者工具(F12)
  2. 转到“网络”选项卡
  3. 重新加载页面
  4. singularsdk-api过滤
  5. 查找发往sdk-api-v1.singular.net的请求
  6. 点击请求查看详情
  7. 验证状态码200
  8. 检查请求负载是否包含您的产品ID。该ID位于负载的"i"参数中。

成功!若看到状态码为200的sdk-api-v1.singular.net 请求,说明您的SDK已成功向Singular发送数据。

验证事件

  1. 在您的网站上触发事件(点击按钮、提交表单等)
  2. 在"网络"选项卡中查找发往sdk-api-v1.singular.net的新请求
  3. 点击该请求,查看"有效负载"或"请求"选项卡
  4. 确认请求中包含事件名称参数"n"

    event_test.png

进行端到端测试时,请使用测试控制台

您可通过Singular仪表盘中的SDK控制台测试Web SDK集成。

  1. 在Singular平台中,前往开发者工具 > 测试控制台
  2. 点击添加设备
  3. 选择平台"Web"并输入Singular设备ID
  4. 从浏览器有效负载中获取Singular设备ID。请参照上方截图定位事件中的SDID 字段。
  5. 测试期间需保持测试控制台处于活动状态(可在独立窗口操作)。控制台关闭或离线时触发的事件将无法显示。
  6. 添加SDID后,可刷新网页并触发若干事件。SDK控制台将显示"__PAGE_VISIT__"及其他已触发的事件。
  7. 通过导出日志报告与洞察导出日志)是验证测试结果的另一种方式。该数据集存在1小时延迟。

步骤7:实现网页到应用的转化追踪

网页到应用归因转发

使用Singular WebSDK追踪用户从网站到移动应用的旅程,实现精准的网页活动归因至应用安装及再互动。请按以下步骤配置网页到应用归因转发(含桌面端用户二维码支持)。

  1. 请参照《网站到移动应用归因转发指南》配置 Singular WebSDK 以实现移动网页归因。
  2. 桌面端网页到应用追踪:
    • 完成上述指南中的配置步骤。
    • 使用Singular移动端网页转应用链接(例如:https://yourlink.sng.link/... )及WebSDK函数buildWebToAppLink(link) ,配合QRCode.js等动态二维码库。
    • 在网页生成编码链接的二维码。桌面端用户可通过移动设备扫描该二维码打开应用,同时传递归因所需的营销活动数据。
    • 探索我们的Singular WebSDK演示,获取二维码生成与网页到应用链接处理的实际案例。

提示!当用户从移动应用内置浏览器(如Facebook、Instagram和TikTok所用)切换至设备原生浏览器时,Singular设备ID可能发生变更,导致归因中断。

为避免此问题,请始终为各广告网络使用正确的Singular跟踪链接格式:


Singular WebSDK 演示

以下是基于文档化Singular WebSDK API实现的简洁而全面的示例。该示例清晰区分了自定义事件转化事件收入事件, 并通过WebSDK支持的网页到应用转发功能实现应用链接支持

您可在本地HTML文件中运行此代码,或将其集成至网站进行高级整合与故障排查。

Singular WebSDK 演示(源代码)
#

将下方代码复制粘贴为HTML文件, 在浏览器中打开即可。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  
  <title>Singular WebSDK Demo</title>
  <script src="https://cdn.jsdelivr.net/npm/qrcodejs/qrcode.min.js"></script>
  <script src="https://web-sdk-cdn.singular.net/singular-sdk/latest/singular-sdk.js"></script>

  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, sans-serif;
      line-height: 1.6;
      color: #1a1d29;
      background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
      padding: 40px 20px;
      min-height: 100vh;
    }

    .container {
      max-width: 800px;
      margin: 0 auto;
      background: white;
      padding: 48px;
      border-radius: 16px;
      box-shadow: 0 4px 24px rgba(30, 41, 59, 0.08);
      border: 1px solid rgba(148, 163, 184, 0.1);
    }

    .header-brand {
      display: flex;
      align-items: center;
      margin-bottom: 32px;
      padding-bottom: 24px;
      border-bottom: 2px solid #f1f5f9;
    }

    .brand-dot {
      width: 12px;
      height: 12px;
      background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
      border-radius: 50%;
      margin-right: 12px;
    }

    h1 {
      font-size: 32px;
      font-weight: 700;
      color: #0f172a;
      letter-spacing: -0.025em;
    }

    h2 {
      font-size: 24px;
      font-weight: 600;
      margin: 48px 0 24px 0;
      color: #1e293b;
      padding-bottom: 12px;
      border-bottom: 2px solid #e2e8f0;
      position: relative;
    }

    h2:before {
      content: '';
      position: absolute;
      bottom: -2px;
      left: 0;
      width: 60px;
      height: 2px;
      background: linear-gradient(90deg, #6366f1, #8b5cf6);
    }

    p {
      margin-bottom: 18px;
      color: #475569;
      font-size: 15px;
    }

    strong {
      color: #1e293b;
      font-weight: 600;
    }

    .form-group {
      margin-bottom: 24px;
    }

    .radio-group {
      display: flex;
      gap: 24px;
      margin-bottom: 24px;
      padding: 16px;
      background: #f8fafc;
      border-radius: 12px;
      border: 1px solid #e2e8f0;
    }

    .radio-group label {
      display: flex;
      align-items: center;
      gap: 10px;
      cursor: pointer;
      font-size: 14px;
      font-weight: 500;
      color: #475569;
      transition: color 0.2s ease;
    }

    .radio-group label:hover {
      color: #1e293b;
    }

    input[type="radio"] {
      width: 20px;
      height: 20px;
      cursor: pointer;
      accent-color: #6366f1;
    }

    input[type="text"] {
      width: 100%;
      padding: 14px 18px;
      font-size: 15px;
      font-weight: 400;
      border: 2px solid #e2e8f0;
      border-radius: 12px;
      transition: all 0.2s ease;
      background: #fafbfc;
      color: #1e293b;
    }

    input[type="text"]:focus {
      outline: none;
      border-color: #6366f1;
      background: white;
      box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
    }

    input[type="text"]::placeholder {
      color: #94a3b8;
      font-weight: 400;
    }

    button {
      padding: 14px 32px;
      font-size: 15px;
      font-weight: 700;
      color: #fff;
      background: linear-gradient(90deg, #2363f6 0%, #1672fe 100%);
      border: none;
      border-radius: 4px;  /* Pill shape */
      cursor: pointer;
      box-shadow: 0 2px 8px rgba(35, 99, 246, 0.12);
      transition: background 0.2s, transform 0.2s;
      letter-spacing: 0.03em;
    }
    button:hover {
      background: linear-gradient(90deg, #1672fe 0%, #2363f6 100%);
      transform: translateY(-2px);
      box-shadow: 0 8px 24px rgba(22, 114, 254, 0.18);
    }
    button:active {
      transform: translateY(0);
    }

    a {
      color: #6366f1;
      text-decoration: none;
      font-weight: 600;
      transition: all 0.2s ease;
      position: relative;
    }

    a:hover {
      color: #4f46e5;
    }

    a:after {
      content: '';
      position: absolute;
      width: 0;
      height: 2px;
      bottom: -2px;
      left: 0;
      background: #6366f1;
      transition: width 0.2s ease;
    }

    a:hover:after {
      width: 100%;
    }

    ol {
      margin-left: 10px;
      margin-bottom: 18px;
    }

    ol li {
      margin-left: 10px;
    }

    .info-box {
      background: linear-gradient(135deg, #eff6ff 0%, #f0f9ff 100%);
      border-left: 4px solid #6366f1;
      padding: 20px;
      margin: 24px 0;
      border-radius: 12px;
      border: 1px solid rgba(99, 102, 241, 0.1);
    }

    .info-box p {
      color: #1e40af;
      margin-bottom: 0;
    }

    .section-label {
      font-size: 12px;
      font-weight: 700;
      color: #6b7280;
      margin-bottom: 10px;
      text-transform: uppercase;
      letter-spacing: 1px;
    }

    .button-group {
      display: flex;
      align-items: center;
      gap: 16px;
      flex-wrap: wrap;
    }

    .analytics-badge {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
      color: white;
      padding: 6px 12px;
      border-radius: 20px;
      font-size: 12px;
      font-weight: 600;
      margin-bottom: 16px;
    }

    .feature-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 16px;
      margin: 24px 0;
    }

    .feature-item {
      padding: 16px;
      background: #f8fafc;
      border-radius: 10px;
      border: 1px solid #e2e8f0;
      text-align: center;
    }

    .feature-item strong {
      display: block;
      color: #6366f1;
      font-size: 14px;
      margin-bottom: 4px;
    }

    .metric-dot {
      width: 8px;
      height: 8px;
      background: #10b981;
      border-radius: 50%;
      display: inline-block;
      margin-right: 8px;
      animation: pulse 2s infinite;
    }

    @keyframes pulse {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.5; }
    }
  </style>

</head>

<body>
  <div class="container">
    <div class="header-brand">
      <div class="brand-dot"></div>
      <h1>Singular WebSDK Demo</h1>
    </div>

    <p>
      Below is a simple but comprehensive implementation using the documented Singular WebSDK API. 
      This sample provides clear separation for <strong>custom events</strong>, <strong>conversion events</strong>, <strong>revenue events</strong>, and <strong>web-to-app link support using WebSDK supported Web-to-App forwarding</strong>. </p>
      <p>You can run this code in a local HTML file or modify it in your site for advanced integration and troubleshooting.
    </p>

    <h2>SDK Initialization Testing</h2>
    <ol>
      <li>Open your browsers developer tools.</li>
      <li>Inspect the Network tab and filter for "sdk-api-v1.singular.net"</li>
      <li>Refresh this page, and examin the payload of the request.</li>
    </ol>


    <hr>

    <h2>User Authentication</h2>
    <p>User authentication is key to supporting cross-device attribution. Pass the common User ID value that would also be sent to Singular from mobile SDKs for cross-device continuity.</p>
    <div class="form-group">
      <div class="section-label">Simulate User Authentication</div>
      <input type="text" id="userId" placeholder="Enter a User ID"/>
    </div>
    <div class="form-group">
      <div class="button-group">
        <button onclick="setUserId()">Set the User ID (Login)</button>
        <button onclick="unsetUserId()">Unset the User ID (Logout)</button>
      </div>
    </div>

    <hr>

    <h2>SDK Event Testing</h2>
    <div class="form-group">
      <div class="section-label">Event Type Selection</div>
      <div class="radio-group">
        <label>
          <input type="radio" name="eventType" value="custom" checked onchange="handleRevenueFields(this)">
          <span>Custom Event</span>
        </label>
        <label>
          <input type="radio" name="eventType" value="conversion" onchange="handleRevenueFields(this)">
          <span>Conversion Event</span>
        </label>
        <label>
          <input type="radio" name="eventType" value="revenue" onchange="handleRevenueFields(this)">
          <span>Revenue Event</span>
        </label>
      </div>
    </div>

    <div class="form-group">
      <div class="section-label">Event Configuration</div>
      <input type="text" id="eventName" placeholder="Enter event name"/>
    </div>

    <div class="form-group" id="currencyGroup" style="display:none">
      <input type="text" id="currency" placeholder="Currency code (e.g., USD, EUR)"/>
    </div>

    <div class="form-group" id="amountGroup" style="display:none">
      <input type="text" id="amount" placeholder="Revenue amount (e.g., 29.99)"/>
    </div>

    <div class="form-group" id="attributesGroup" style="display:none">
      <input type="text" id="attributes" placeholder='Optional attributes (JSON, e.g. {"color":"blue"})'/>
    </div>

    <div class="form-group">
      <button id="send" onclick="sendCustomEvent()">Send Event to Singular</button>
    </div>

    <hr>
    <h2>Web-to-App Forwarding</h2>
    <div class="form-group">
    <ol>
      <li><a href="https://support.singular.net/hc/zh-cn/articles/360042283811" target="_blank">Learn more about Web-to-App Forwarding</a></li>
    <li>In the page source code you will find the mobile web-to-app base link "<strong>https://seteam.sng.link/D0mvs/y3br?_smtype=3</strong>" used for demonstration purposes.</li>
    <li>Replace all occurances of the link with your own Singular Mobile Web-to-App link to test functionality.</li>
    <li>Add Singular Web Parameters to the URL and refresh the page. (example: "<strong>?wpsrc=SingularTest&wpcn=MyCampaign</strong>").</li>
  </ol>

    <hr>
    <div class="section-label">Mobile Web-to-App Test</div>
    <div class="form-group">
      <div class="button-group">
        <button onclick="displayWebLink()">Display Constructed Web-to-App Link</button>
        <button onclick="testWebLink()">Test Web-to-App Link</button>
      </div>
    </div>
  </div>
    <hr>
    <div class="section-label">Desktop Web-to-App Configuration</div>
    <div class="form-group">
    <p>Use a dynamic QR Code generation library like <a href="https://davidshimjs.github.io/qrcodejs/" target="_blank">QRCode.js</a> to build and display a QR code using the Singular Mobile Web-to-App link with constructed campaign parameters.</p>
    <div id="qrcode"></div>
    </div>

  </div>
  
  <script>
    /**
     * Initializes the Singular SDK with the provided configuration.
     * @param {string} sdkKey - The SDK key for Singular (e.g., 'se_team_9b3431b0').
     * @param {string} sdkSecret - The SDK secret for Singular (e.g., 'bcdee06e8490949422c071437da5c5ed').
     * @param {string} productId - The product ID for Singular (e.g., 'com.website').
     * @example
     * // Initialize the Singular SDK
     * initSingularSDK('se_team_9b3431b0', 'bcdee06e8490949422c071437da5c5ed', 'com.website');
     */
    function initSingularSDK() {
      var sdkKey = 'se_team_9b3431b0';
      var sdkSecret = 'bcdee06e8490949422c071437da5c5ed';
      var productId = 'com.website';

      // Global Properties set during init (plain object; max 5 keys persisted).
      const globalProperties = {
        plan: "pro",
        region: "us"
      };

      // Applies to the whole object: false = merge with existing, true = clear existing then set.
      const overrideExistingGlobalProperties = false; // required

      const config = new SingularConfig(sdkKey, sdkSecret, productId)
        .withLogLevel(3)
        .withSessionTimeoutInMinutes(0.5)
        .withGlobalProperties(globalProperties, overrideExistingGlobalProperties)
        .withInitFinishedCallback(function (initParams) {
          console.log("Singular Device ID:", initParams.singularDeviceId);
          console.log("Global props (SDK):", window.singularSdk.getGlobalProperties());

          // If you need to override just one key after init set it in the withInitFinishedCallback:
          window.singularSdk.setGlobalProperties("plan", "enterprise");
    });

      // Initialize the Singular SDK
      window.singularSdk.init(config);
      generateDesktopWebToAppQRCode(); // Generate QR code after initialization
    }

    /**
     * Triggers Singular SDK initialization when the DOM is fully parsed.
     * @example
     * document.addEventListener('DOMContentLoaded', function() {
     *   initSingularSDK();
     * });
     */
    document.addEventListener('DOMContentLoaded', function() {
      initSingularSDK();
    });


    // Fires a custom event using the Singular SDK
    function sendCustomEvent() {
      var eventName = document.getElementById('eventName').value;
      window.singularSdk.event(eventName);
    }

    // Fires a conversion event using the Singular SDK
    function sendConversionEvent() {
      var eventName = document.getElementById('eventName').value;
      window.singularSdk.conversionEvent(eventName);
    }

    // Fires a revenue event only once per session with deduplication
    // Parameters:
    //   - eventName: string (name of event), default "web_purchase" if blank
    //   - amount: revenue amount, default 0 if blank
    //   - currency: string, defaults to "USD" if blank
    //   - attributes: optional object with extra payload
    function sendRevenueEvent(eventName, amount, currency, attributes) {
      // Fill in defaults and normalize values
      eventName = eventName ? eventName : "web_purchase";
      currency = currency ? currency.toUpperCase() : "USD";
      amount = amount ? amount : 0;

      // Create a unique key based on event data for deduplication
      // btoa(JSON.stringify(...)) ensures order consistency in local/sessionStorage
      var payload = {
        eventName: eventName,
        amount: amount,
        currency: currency,
        attributes: attributes ? attributes : null
      };
      var storageKey = 'singular_revenue_' + btoa(JSON.stringify(payload));

      // Only fire event if no identical event has already fired in this tab/session
      if (!sessionStorage.getItem(storageKey)) {
        sessionStorage.setItem(storageKey, 'true');
        if (attributes && typeof attributes === 'object' && Object.keys(attributes).length > 0) {
          // Fire event with attributes payload
          window.singularSdk.revenue(eventName, currency, amount, attributes);
        } else {
          // Fire simple revenue event
          window.singularSdk.revenue(eventName, currency, amount);
        }
        console.log("Revenue event sent:", payload);
      } else {
        // Duplicate detected, don't send to SDK
        console.log("Duplicate revenue event prevented:", payload);
        alert("Duplicate revenue event prevented!");
      }
    }

    // Collects form values, validates attributes JSON, and calls sendRevenueEvent()
    // Ensures only valid values are sent and blocks on malformed JSON
    function fireFormRevenueEvent() {
      var eventName = document.getElementById('eventName').value;
      var currency = document.getElementById('currency').value;
      var amount = document.getElementById('amount').value;
      
      var attributesRaw = document.getElementById('attributes').value;
      var attributes = null;
      if (attributesRaw) {
        try {
          // Try to parse the optional attributes field from JSON string
          attributes = JSON.parse(attributesRaw);
        } catch (e) {
          alert("Optional attributes must be valid JSON.");
          return; // Stop if invalid JSON
        }
      }

      // Calls main revenue logic for deduplication and event sending
      sendRevenueEvent(eventName, amount, currency, attributes);
    }

    // Controls which form fields are visible depending on selected event type
    // Revenue events require currency, amount, and attributes fields
    function handleRevenueFields(sender) {
      var isRevenue = sender.value === "revenue";
      document.getElementById("currencyGroup").style.display = isRevenue ? "block" : "none";
      document.getElementById("amountGroup").style.display = isRevenue ? "block" : "none";
      document.getElementById("attributesGroup").style.display = isRevenue ? "block" : "none";

      // Dynamically assign the event button's onclick handler
      var send = document.getElementById("send");
      send.onclick = (sender.value === "custom") ? sendCustomEvent
        : (sender.value === "conversion") ? sendConversionEvent
        : fireFormRevenueEvent; // Only fires revenue logic for "revenue"
    }

    // Opens demo Singular web-to-app link in a new tab/window
    function testWebLink() {
      var singularWebToAppBaseLink = 'https://seteam.sng.link/D0mvs/y3br?_smtype=3';
      window.open(singularWebToAppBaseLink);
    }

    // Displays constructed Singular web-to-app link with campaign parameters
    function displayWebLink() {
      var singularWebToAppBaseLink = 'https://seteam.sng.link/D0mvs/y3br?_smtype=3';
      var builtLink = window.singularSdk.buildWebToAppLink(singularWebToAppBaseLink);
      console.log("Singular Web-to-App Link: ", builtLink);
      alert("Singular Web-to-App Link: " + builtLink);
    }

    // Generates QR code for desktop deep linking using Singular Mobile Web-to-App link
    function generateDesktopWebToAppQRCode() {
      var singularWebToAppBaseLink = 'https://seteam.sng.link/D0mvs/y3br?_smtype=3';
      const value = window.singularSdk.buildWebToAppLink(singularWebToAppBaseLink);
      new QRCode(document.getElementById("qrcode"), {
        text: value,
        width: 128,
        height: 128,
        colorDark: "#000",
        colorLight: "#fff",
        correctLevel: QRCode.CorrectLevel.H
      });
    }

    // Simulate user authentication and send login event
    function setUserId() {
      var userId = document.getElementById('userId').value;
      window.singularSdk.login(userId);
      console.log("Singular User ID is Set to: " + userId);
      window.singularSdk.event("sng_login");
    }

    // Simulate user logout and unset Singular user ID
    function unsetUserId() {
      window.singularSdk.logout();
      console.log("Singular User ID is Unset");
    }
  </script>
</body>
</html>

进阶主题

Singular横幅广告

启用 Singular 横幅广告
#

提示:单一横幅为企业级功能。 如需了解详情,请联系您的客户成功经理。

在移动网站中展示Singular横幅, 可无缝引导网页用户进入应用并展示 最相关的应用内容。启用网站横幅功能后, 您的组织可通过Singular横幅界面轻松设计、部署 及维护横幅内容。

相关文章

分步实施指南

  1. 将Singular原生JavaScript WebSDK脚本添加至您的 网站。

    在实施横幅前,请遵循 上述集成指南将Singular原生JavaScript WebSDK添加至您的网站。

  2. 授予Singular访问客户端提示数据的权限

    鉴于2023年2-3月Chromium浏览器对用户代理数据的限制,广告主现需获取客户端提示数据,并授权Singular WebSDK接收该数据。更多信息请参阅横幅广告常见问题解答

    网站需执行以下操作:

    • 开始请求客户端提示数据(accept-ch header )。
    • 授予Singular权限(作为第三方),使 浏览器 在获取横幅广告请求时向Singular发送客户端提示数据 (详见permissions-policy header )。

    在页面加载响应中添加以下响应头:

    accept-ch:
    sec-ch-ua-model,
    sec-ch-ua-platform-version,
    sec-ch-ua-full-version-list
    
    permissions-policy:
    ch-ua-model=("https://sdk-api-v1.singular.net"),
    ch-ua-platform-version=("https://sdk-api-v1.singular.net"),
    ch-ua-full-version-list=("https://sdk-api-v1.singular.net")
  3. 在 WebSDK 初始化时添加横幅配置选项

    Singular横幅可在移动网站上智能显示应用下载提示, 同时保留原始营销来源的归因信息。启用 网页到应用支持功能后, SDK将追踪用户通过哪个广告网络或活动进入网站, 并将后续应用安装行为归因至原始来源。

    /**
     * Initialize Singular SDK with Banner support and web-to-app attribution.
     * This enables smart banners that direct mobile web users to download your app
     * while preserving the original marketing source (UTM parameters, ad network, etc.)
     * for proper attribution when the app is installed.
     * @param {string} sdkKey - Your Singular SDK Key
     * @param {string} sdkSecret - Your Singular SDK Secret
     * @param {string} productId - Your Product ID (e.g., com.your-website)
     */
    function initSingularSDK() {
      // Enable web-to-app tracking to preserve attribution data
      var bannersOptions = new BannersOptions().withWebToAppSupport();
      
      // Configure SDK with banner support
      var config = new SingularConfig(sdkKey, sdkSecret, productId)
        .withBannersSupport(bannersOptions);
      
      // Initialize the SDK
      window.singularSdk.init(config);
      
      // Display the smart banner
      window.singularSdk.showBanner();
    }
    
    // Call on DOM ready
    document.addEventListener('DOMContentLoaded', function() {
      initSingularSDK();
    });
  4. 在页面路由中重新显示横幅(仅限单页应用)

    若您的应用为单页应用,需在每个页面路由中隐藏并重新显示横幅。此操作可确保Singular为您的网页体验投放精准匹配的横幅。

    隐藏并重新显示横幅请使用以下代码:

    window.singularSdk.hideBanner();
    window.singularSdk.showBanner();
  5. [高级选项] 自定义链接设置

    Singular支持通过代码实现横幅内链接的个性化设置。

    个性化链接步骤:

    • 创建LinkParams 对象,并调用下列一项或多项函数。此操作需在调用window.singularSdk.showBanner() 前完成。
    • 随后在调用window.singularSdk.showBanner() 时传递该LinkParams 对象。

    示例:

    // Define a LinkParams object
    let bannerParams = new LinkParams();
    
    // Configure link options (see details on each option below)
    bannerParams.withAndroidRedirect("androidRedirectValue");
    bannerParams.withAndroidDL("androidDLParamValue");
    bannerParams.withAndroidDDL("androidDDLparamValue");
    bannerParams.withIosRedirect("iosRedirectValue");
    bannerParams.withIosDL("iosDLValue");
    bannerParams.withIosDDL("iosDDLValue");
    
    // Show the banner with the defined options
    window.singularSdk.showBanner(bannerParams);

    选项列表:

    方法 描述
    withAndroidRedirect 传递指向Android应用下载页面的重定向链接 通常为Google Play商店页面。
    withAndroidDL 传递应用内页面的深度链接。
    withAndroidDDL 传递延迟深层链接,即指向用户尚未安装的 Android应用内 页面的链接。
    withIosRedirect 传递重定向链接至您的 iOS 应用下载 页面, 通常为 App Store 页面。
    withIosDL 传递指向iOS应用内页面的深度链接。
    withIosDDL 传递延迟深层链接,即指向用户尚未安装的iOS应用内页面的链接。
  6. [高级选项] 使用代码强制隐藏/显示横幅

    如步骤3所述,若采用单页应用架构, 需在每个页面路由中调用 hideBanner()showBanner() 方法 以确保 正确展示横幅(参见上文)。

    hideBanner() 若需隐藏本应显示的横幅或重新显示已隐藏的横幅, 您还可在代码中调用showBanner()方法实现。

    方法 描述
    singularSdk.hideBanner() 隐藏页面中可见的横幅。
    singularSdk.showBanner() 显示预配置横幅。
    singularSdk.showBanner(params) 显示预配置横幅,但通过 linkParams 对象 (参见步骤 4) 定义的选项覆盖 链接 参数。

全局属性

全局属性
#

Singular SDK 允许您定义自定义属性,这些属性将随应用程序发送的每次会话和事件一同发送到 Singular 服务器。这些属性可代表您希望获取的任何信息,包括用户信息、应用程序模式/状态或其他任何内容。

  • 最多可定义5个全局属性, 以有效JSON对象形式存储。 全局属性将保存在浏览器的 localstorage 中, 直至被清除或浏览器上下文变更。

  • 每个属性名称和值最长可达200个字符。 若传递的属性名称或值超出此限制, 将被截断为200个字符。

  • 全局属性目前会反映在Singular的 用户级事件日志(参见《导出归因日志》) 及回传数据中。

  • 全局属性可用于Singular向第三方发送回传数据时 进行匹配操作。

若要在WebSDK初始化阶段设置全局属性, 必须在配置对象中实现.withGlobalProperties()选项。

初始化后处理全局属性时,必须使用 SDK函数:setGlobalProperties()getGlobalProperties()clearGlobalProperties()

.withGlobalProperties()setGlobalProperties()getGlobalProperties()clearGlobalProperties()
/**
 * Set a Singular global property during SDK initialization.
 * Allows up to 5 key/value pairs. Optionally overwrites existing value for a key.
 * @param {{[key: string]: string}} globalProperties - The global property key/value pair object.
 * @param {string} propertyKey - The property key to set.
 * @param {string} propertyValue - The property value to set.
 * @param {boolean} overrideExisting - Whether to overwrite the property if it already exists.
 */
            
 // Global Properties set during init (plain object; max 5 keys persisted).
 var globalProperties = {
   propertyKey: propertyValue
 };

 // Set the override, applies to the whole object: false = merge with existing, true = clear existing then set.
 var overrideExisting = false; // required

 var config = new SingularConfig(sdkKey, sdkSecret, productId)
   .withLogLevel(3)
   .withSessionTimeoutInMinutes(0.5)
   .withGlobalProperties(globalProperties, overrideExisting)
   .withInitFinishedCallback(function (initParams) {
      console.log("Singular Device ID:", initParams.singularDeviceId);
      console.log("Global props (SDK):", window.singularSdk.getGlobalProperties());

      // If you need to override just one key after init set it in the withInitFinishedCallback:
      window.singularSdk.setGlobalProperties("plan", "enterprise");
  });

  // Initialize the Singular SDK
  window.singularSdk.init(config);           
xml-ph-0000@deepl.internal

自然搜索追踪

自然搜索追踪示例
#

重要提示!本示例作为 临时解决方案 提供,用于启用自然搜索跟踪功能。该代码仅供参考, 应由 网站开发人员根据营销部门需求进行更新维护。 自然搜索 跟踪对不同广告主可能具有不同含义。 请 审阅示例并根据实际需求调整。

为何使用此方案?

  • 确保自然搜索访问被正确追踪, 即使未携带广告系列参数。

  • 在URL末尾添加Singular形式的"来源"参数wpsrc(取值自推荐来源),并添加"活动名称"参数wpcn (取值设为"OrganicSearch"), 实现清晰归因。

  • 将当前URL和来源网址存储在localStorage中 供后续使用。

  • 纯 JavaScript 实现,零依赖,轻松集成。

工作原理

  1. 检查页面URL是否包含已知营销活动参数 (来自Google、Facebook、TikTok、UTM等)。

  2. 若未检测到营销参数且来源为搜索引擎,则追加:

    • wpsrc (将来源网址设为其值)
    • wpcn (值为OrganicSearch)
  3. 更新浏览器中的URL,无需重新加载页面。

  4. 将当前URL和引荐来源存储在localStorage中: sng_urlsng_ref

使用方法

  1. setupOrganicSearchTracking.js 作为库添加到 您的网站中。
  2. 初始化WebSDK之前 调用setupOrganicSearchTracking() 函数。
setupOrganicSearchTracking.js
// singular-web-organic-search-tracking: setupOrganicSearchTracking.js
// Tracks organic search referrals by appending wpsrc and wpcn to the URL if no campaign parameters exist and the referrer is a search engine.

// Configuration for debugging (set to true to enable logs)
const debug = true;

// List of campaign parameters to check for exclusion
const campaignParams = [
    'gclid', 'fbclid', 'ttclid', 'msclkid', 'twclid', 'li_fat_id',
    'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'wpsrc'
];

// Whitelist of legitimate search engine domains (prevents false positives)
const legitimateSearchEngines = new Set([
    // Google domains
    'google.com', 'google.co.uk', 'google.ca', 'google.com.au', 'google.de', 
    'google.fr', 'google.it', 'google.es', 'google.co.jp', 'google.co.kr',
    'google.com.br', 'google.com.mx', 'google.co.in', 'google.ru', 'google.com.sg',
    
    // Bing domains  
    'bing.com', 'bing.co.uk', 'bing.ca', 'bing.com.au', 'bing.de',
    
    // Yahoo domains
    'yahoo.com', 'yahoo.co.uk', 'yahoo.ca', 'yahoo.com.au', 'yahoo.de',
    'yahoo.fr', 'yahoo.it', 'yahoo.es', 'yahoo.co.jp',
    
    // Other search engines
    'baidu.com', 'duckduckgo.com', 'yandex.com', 'yandex.ru',
    'ask.com', 'aol.com', 'ecosia.org', 'startpage.com', 
    'qwant.com', 'seznam.cz', 'naver.com', 'daum.net'
]);

// Extract main domain from hostname (removes subdomains)
function getMainDomain(hostname) {
    if (!hostname) return '';
    
    const lowerHost = hostname.toLowerCase();
    
    // Handle special cases for known search engines with country codes
    const searchEnginePatterns = {
        'google': (host) => {
            // Match google.TLD patterns more precisely
            if (host.includes('google.co.') || host.includes('google.com')) {
                const parts = host.split('.');
                const googleIndex = parts.findIndex(part => part === 'google');
                if (googleIndex !== -1 && googleIndex < parts.length - 1) {
                    return parts.slice(googleIndex).join('.');
                }
            }
            return null;
        },
        'bing': (host) => {
            if (host.includes('bing.co') || host.includes('bing.com')) {
                const parts = host.split('.');
                const bingIndex = parts.findIndex(part => part === 'bing');
                if (bingIndex !== -1 && bingIndex < parts.length - 1) {
                    return parts.slice(bingIndex).join('.');
                }
            }
            return null;
        },
        'yahoo': (host) => {
            if (host.includes('yahoo.co') || host.includes('yahoo.com')) {
                const parts = host.split('.');
                const yahooIndex = parts.findIndex(part => part === 'yahoo');
                if (yahooIndex !== -1 && yahooIndex < parts.length - 1) {
                    return parts.slice(yahooIndex).join('.');
                }
            }
            return null;
        }
    };
    
    // Try specific patterns for major search engines
    for (const [engine, patternFn] of Object.entries(searchEnginePatterns)) {
        if (lowerHost.includes(engine)) {
            const result = patternFn(lowerHost);
            if (result) return result;
        }
    }
    
    // Handle other known engines with simple mapping
    const otherEngines = {
        'baidu.com': 'baidu.com',
        'duckduckgo.com': 'duckduckgo.com', 
        'yandex.ru': 'yandex.ru',
        'yandex.com': 'yandex.com',
        'ask.com': 'ask.com',
        'aol.com': 'aol.com',
        'ecosia.org': 'ecosia.org',
        'startpage.com': 'startpage.com',
        'qwant.com': 'qwant.com',
        'seznam.cz': 'seznam.cz',
        'naver.com': 'naver.com',
        'daum.net': 'daum.net'
    };
    
    for (const [domain, result] of Object.entries(otherEngines)) {
        if (lowerHost.includes(domain)) {
            return result;
        }
    }
    
    // Fallback: Extract main domain by taking last 2 parts (for unknown domains)
    const parts = hostname.split('.');
    if (parts.length >= 2) {
        return parts[parts.length - 2] + '.' + parts[parts.length - 1];
    }
    
    return hostname;
}

// Get query parameter by name, using URL.searchParams with regex fallback for IE11
function getParameterByName(name, url) {
    if (!url) url = window.location.href;
    try {
        return new URL(url).searchParams.get(name) || null;
    } catch (e) {
        if (debug) console.warn('URL API not supported, falling back to regex:', e);
        name = name.replace(/[\[\]]/g, '\\$&');
        const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
        const results = regex.exec(url);
        if (!results) return null;
        if (!results[2]) return '';
        return decodeURIComponent(results[2].replace(/\+/g, ' '));
    }
}

// Check if any campaign parameters exist in the URL
function hasAnyParameter(url, params) {
    return params.some(param => getParameterByName(param, url) !== null);
}

// Improved search engine detection - only checks hostname, uses whitelist
function isSearchEngineReferrer(referrer) {
    if (!referrer) return false;
    
    let hostname = '';
    try {
        hostname = new URL(referrer).hostname.toLowerCase();
    } catch (e) {
        // Fallback regex for hostname extraction
        const match = referrer.match(/^(?:https?:\/\/)?([^\/\?#]+)/i);
        hostname = match ? match[1].toLowerCase() : '';
    }
    
    if (!hostname) return false;
    
    // First check: exact match against whitelist
    if (legitimateSearchEngines.has(hostname)) {
        if (debug) console.log('Exact match found for:', hostname);
        return true;
    }
    
    // Second check: subdomain of legitimate search engine
    const hostParts = hostname.split('.');
    if (hostParts.length >= 3) {
        // Try domain.tld combination (e.g., google.com from www.google.com)
        const mainDomain = hostParts[hostParts.length - 2] + '.' + hostParts[hostParts.length - 1];
        if (legitimateSearchEngines.has(mainDomain)) {
            if (debug) console.log('Subdomain match found for:', hostname, '-> main domain:', mainDomain);
            return true;
        }
        
        // Try last 3 parts for country codes (e.g., google.co.uk from www.google.co.uk)
        if (hostParts.length >= 3) {
            const ccDomain = hostParts[hostParts.length - 3] + '.' + hostParts[hostParts.length - 2] + '.' + hostParts[hostParts.length - 1];
            if (legitimateSearchEngines.has(ccDomain)) {
                if (debug) console.log('Country code domain match found for:', hostname, '-> cc domain:', ccDomain);
                return true;
            }
        }
    }
    
    if (debug) {
        console.log('Hostname not recognized as legitimate search engine:', hostname);
    }
    
    return false;
}

// Main function to update URL with organic search tracking parameters
function setupOrganicSearchTracking() {
    const url = window.location.href;
    const referrer = document.referrer || '';

    // Store URL and referrer in localStorage
    try {
        localStorage.setItem('sng_url', url);
        localStorage.setItem('sng_ref', referrer);
    } catch (e) {
        if (debug) console.warn('localStorage not available:', e);
    }

    if (debug) {
        console.log('Current URL:', url);
        console.log('Referrer:', referrer);
    }

    // Skip if campaign parameters exist or referrer is not a search engine
    const hasCampaignParams = hasAnyParameter(url, campaignParams);
    if (hasCampaignParams || !isSearchEngineReferrer(referrer)) {
        if (debug) console.log('Skipping URL update: Campaign params exist or referrer is not a legitimate search engine');
        return;
    }

    // Extract and validate referrer hostname
    let referrerHostname = '';
    try {
        referrerHostname = new URL(referrer).hostname;
    } catch (e) {
        if (debug) console.warn('Invalid referrer URL, falling back to regex:', e);
        referrerHostname = referrer.match(/^(?:https?:\/\/)?([^\/]+)/i)?.[1] || '';
    }

    // Extract main domain from hostname
    const mainDomain = getMainDomain(referrerHostname);
    
    if (debug) {
        console.log('Full hostname:', referrerHostname);
        console.log('Main domain:', mainDomain);
    }

    // Only proceed if main domain is valid and contains safe characters
    if (!mainDomain || !/^[a-zA-Z0-9.-]+$/.test(mainDomain)) {
        if (debug) console.log('Skipping URL update: Invalid or unsafe main domain');
        return;
    }

    // Update URL with wpsrc and wpcn parameters
    const urlObj = new URL(url);
    
    // Set wpsrc to the main domain (e.g., google.com instead of tagassistant.google.com)
    urlObj.searchParams.set('wpsrc', mainDomain);
    
    // Set wpcn to 'Organic Search' to identify the campaign type
    urlObj.searchParams.set('wpcn', 'Organic Search');

    // Update the URL without reloading (check if history API is available)
    if (window.history && window.history.replaceState) {
        try {
            window.history.replaceState({}, '', urlObj.toString());
            if (debug) console.log('Updated URL with organic search tracking:', urlObj.toString());
        } catch (e) {
            if (debug) console.warn('Failed to update URL:', e);
        }
    } else {
        if (debug) console.warn('History API not supported, cannot update URL');
    }
}

跨子域追踪

默认情况下,Singular WebSDK会生成Singular设备ID并通过浏览器存储进行持久化。由于子域之间无法共享此存储空间,SDK最终会为每个子域生成新的ID。

若需跨子域持久化 Singular 设备 ID,可采用以下任一方案:

方法 B(高级):手动设置唯一设备 ID
#

方法 B(高级):手动设置 Singular 设备 ID

若不希望 Singular SDK 自动持久化设备 ID, 可通过 跨域手动保存 ID(例如使用 顶级域 Cookie 或服务器端 Cookie)。该值应为 Singular 先前 生成的 有效 uuid4 格式 ID。

注意:调用init方法后或通过InitFinishedCallback回调, 可使用singularSdk.getSingularDeviceId()读取Singular设备ID。

withPersistentSingularDeviceId 方法

描述

使用包含需持久化的Singular设备ID的配置选项初始化SDK。

签名 withPersistentSingularDeviceId(singularDeviceId)
用法示例
function initSingularSDK() {
  const config = new SingularConfig(sdkKey, sdkSecret, productId)
    .withPersistentSingularDeviceId(singularDeviceId);
  window.singularSdk.init(config);
}

后续步骤

相关文章