网络 SDK - 本地 JavaScript 实施指南

文件

概述

信息:Web Attribution 是一项企业功能。请联系您的客户成功经理,为您的账户启用该功能。

本指南介绍如何使用本地 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 AppbundleID相匹配。
  • 编辑网站 HTML 代码的权限。
  • 在页面的<head> 部分添加 JavaScript 的权限。
  • 要跟踪的事件列表。查看我们的Singular 标准事件:完整列表和按垂直领域推荐的事件,以了解相关想法。

实施步骤


第 1 步:添加 SDK 库脚本

将以下代码段添加到网站每个页面的<head> 部分。尽早放置,最好靠近<head> 标签的顶部。

提示!尽早添加脚本可确保页面源代码中的任何 Singular 函数都能使用 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__事件用于在满足以下条件时在服务器端生成一个新的会话
    • 用户到达时 URL 中包含新的广告数据(如 UTM 或 WP 参数),或
    • 上一个会话已过期(30 分钟未活动后)。
    • 会话用于衡量用户保留率并支持重新参与归因。
  1. 创建初始化函数,并在页面加载后的 DOM 就绪时调用该函数。
  2. 确保初始化发生在报告任何其他 Singular 事件之前。
  3. 对于单页面应用程序 (SPA),应在首次页面加载时初始化 Singular SDK,然后在代表新的页面视图的每次路由变化时调用 Singular Page Visist 函数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 就绪后运行代码(例如,获取奇异设备 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 初始化时调用回调函数 初始化完成时调用回调函数

开发人员核对表

  • 收集 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方法向Singular发送内部用户ID。

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

  • 用户 ID 可以是任何标识符,但不能暴露 PII(个人身份信息),例如,不能使用用户的电子邮件地址、用户名或电话号码。Singular 建议使用仅对第一方数据唯一的哈希值。
  • 传递给Singular的用户ID值也应该是你在所有平台(Web/移动/PC/控制台/离线)上获取的相同的内部用户ID。
  • Singular 将在用户级导出、ETL 和内部 BI 回传(如果配置)中包含用户 ID。用户 ID 是第一方数据,Singular 不会与其他方共享。
  • 使用 Singular SDK 方法设置的用户 ID 值将一直存在,直到使用logout() 方法取消设置或删除浏览器本地存储。关闭或刷新网站不会取消设置用户 ID。
  • 在私人/隐身模式下,SDK 无法持续设置用户 ID,因为浏览器会在关闭时自动删除本地存储。

要设置用户 ID,请使用login() 方法。要取消设置(例如,如果用户 "注销 "账户),请调用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:防止重复事件

重要!这是最常见的实施错误之一。如果没有适当的防护措施,事件可能会多次触发,从而夸大您的指标。

在网络应用程序中防止重复收入事件对于准确分析和避免过度报告转换非常重要。我们的目标是确保每个唯一事件在每个会话或页面访问中只触发一次,即使用户重新加载了页面或无意中触发了多次提交。

  • 每个事件都会生成一个唯一密钥(基于事件详细信息和可选自定义属性)。
  • 该密钥存储在浏览器的会话存储(或用于持久重复数据删除的本地存储)中。
  • 在发送之前,代码会检查是否已经触发了具有相同有效载荷的收入事件。
  • 如果没有,就会发送事件并保存密钥。
  • 如果是,则会阻止重复并通知用户或开发人员。
Using Session Storage Method

使用会话存储方法

// Sends a revenue event to Singular WebSDK, preventing duplicates in the same session
// @param {string} eventName - Name of the revenue event (defaults to "web_purchase")
// @param {number} amount - Revenue amount (defaults to 0)
// @param {string} currency - Currency code (defaults to "USD", normalized to uppercase)
// @param {object} [attributes] - Optional key-value pairs for additional event data
function sendRevenueEvent(eventName, amount, currency, attributes) {
  // Normalize inputs: set defaults and ensure currency is uppercase
  eventName = eventName ? eventName : "web_purchase";
  currency = currency ? currency.toUpperCase() : "USD";
  amount = amount ? amount : 0;

  // Create a payload object to standardize event data
  var payload = {
    eventName: eventName,
    amount: amount,
    currency: currency,
    attributes: attributes ? attributes : null // Null if no attributes provided
  };

  // Generate a unique key for sessionStorage by hashing the payload
  // This ensures deduplication of identical events in the same session
  var storageKey = 'singular_revenue_' + btoa(JSON.stringify(payload));

  // Check if the event was already sent in this session to prevent duplicates
  if (!sessionStorage.getItem(storageKey)) {
    // Mark event as sent in sessionStorage
    sessionStorage.setItem(storageKey, 'true');

    // Send revenue event to Singular SDK, including attributes if provided and valid
    if (attributes && typeof attributes === 'object' && Object.keys(attributes).length > 0) {
      window.singularSdk.revenue(eventName, currency, amount, attributes);
    } else {
      // Fallback to basic revenue event without attributes
      window.singularSdk.revenue(eventName, currency, amount);
    }
    // Log event details for debugging
    console.log("Revenue event sent:", payload);
  } else {
    // Log and alert if a duplicate event is detected
    console.log("Duplicate revenue event prevented:", payload);
    alert("Duplicate revenue event prevented!");
  }
}

第 6 步:测试实现

实施 SDK 后,使用浏览器的开发工具验证其是否正常工作。

验证已加载 SDK

  1. 在浏览器中打开网站
  2. 打开开发工具(F12 或右键单击 → 检查)
  3. 转到控制台选项卡
  4. 输入typeof singularSdk 并按 Enter
  5. 您应该看到一个 "函数 "对象,而不是 "未定义 "对象

验证网络请求

  1. 打开 "开发工具"(F12)
  2. 转到网络选项卡
  3. 重新加载页面
  4. singularsdk-api过滤
  5. 查找指向sdk-api-v1.singular.net的请求
  6. 单击请求查看详细信息
  7. 验证状态代码是否为200
  8. 检查请求有效载荷是否包含您的产品 ID。这将出现在有效负载的 "i"参数中。

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

验证事件

  1. 在网站上触发一个事件(点击按钮、提交表单等)
  2. 在 "网络"选项卡中,查找指向sdk-api-v1.singular.net的新请求
  3. 点击请求并查看有效载荷请求选项卡
  4. 确认请求中出现了事件名称 "n" 参数

    event_test.png

使用测试控制台进行端到端测试

您可以使用 Singular 面板上的SDK 控制台测试 WebSDK集成。

  1. 在 Singular 平台中,转到开发工具 > 测试控制台
  2. 单击 "添加设备"。
  3. 选择平台"Web"并输入Singular 设备 ID
  4. 从浏览器有效载荷中获取 Singular 设备 ID。查看上面的截图,在事件上找到SDID
  5. 在单独窗口中进行测试时,测试控制台必须保持激活状态。控制台关闭或离线时不会显示任何触发的事件。
  6. 添加 SDID 后,就可以重新加载网页并触发一些事件。SDK 控制台将显示"__PAGE_VISIT___"和其他事件(如果触发)。
  7. 利用导出日志(报告和洞察导出日志)是验证测试结果的另一种方法。此数据集延迟 1 小时。

步骤 7:实施网络到应用程序转发

网络到应用程序归因转发

使用 Singular WebSDK 跟踪从网站到移动应用的用户旅程,从而将网络营销活动准确归因于移动应用的安装和重新吸引。按照以下步骤设置网站到应用程序的转发,包括为桌面用户提供 QR 码支持。

  1. 按照我们的《网站到移动应用归因转发指南》,为移动网络归因配置 Singular WebSDK。
  2. 对于桌面网络到应用程序跟踪:
    • 完成上述指南中的设置。
    • 使用 Singular Mobile Web-to-App Link(如https://yourlink.sng.link/... )和 WebSDK 函数buildWebToAppLink(link) 与动态 QR 码库(如QRCode.js)。
    • 在网页上生成编码链接的二维码。桌面用户可使用移动设备扫描该二维码以打开应用程序,并传递活动数据以进行归因。
    • 请浏览我们的Singular WebSDK 演示,了解 QR 代码生成和网页到应用程序链接处理的工作示例。

提示!移动应用内浏览器网页视图(如 Facebook、Instagram 和 TikTok 使用的视图)会导致用户移动到设备的本地浏览器时,Singular 设备 ID 发生变化,从而中断归因。

为避免这种情况,请始终为每个广告网络使用正确的 Singular 跟踪链接格式:


Singular WebSDK 演示

下面是使用已记录的 Singular WebSDK API 实现的一个简单但全面的示例。该示例使用 WebSDK 支持的 Web 到应用程序转发功能,明确区分了自定义事件转换事件收入事件网络到应用程序链接支持

您可以在本地 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';
      
      var config = new SingularConfig(sdkKey, sdkSecret, productId)
        .withInitFinishedCallback(function(initParams) {
          var singularDeviceId = initParams.singularDeviceId;
          console.log('Singular Device ID:', singularDeviceId);
          // Optionally store for use in events
          // sessionStorage.setItem('singularDeviceId', singularDeviceId);
        });

      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 Banners 后,您的组织可以通过 Singular Banners 用户界面轻松设计、部署和维护横幅广告。

相关文章

分步实施指南

  1. 将 Singular Native 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 横幅会在您的移动网站上显示智能应用程序下载提示,同时保留原始营销来源的属性。当启用Web 到应用程序支持时,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 应用程序下载页面(通常是 Play Store 页面)的重定向链接。
    withAndroidDL 传递指向 Android 应用程序中某个页面的深度链接。
    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 初始化之前设置全局属性,您必须实现自定义setGlobalPropertyBeforeInit函数,并在 SDK 初始化之前调用该函数。如果不需要在初始化之前设置全局属性,则可以省略这段自定义代码。

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

setGlobalPropertyBeforeInit()setGlobalProperties()getGlobalProperties()clearGlobalProperties()
/**
 * Set a Singular global property before SDK initialization.
 * Allows up to 5 key/value pairs. Optionally overwrites existing value for a key.
 * 
 * @param {string} sdkKey - The Singular SDK key.
 * @param {string} productId - The Singular product ID.
 * @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.
 */
function setGlobalPropertyBeforeInit(sdkKey, productId, propertyKey, propertyValue, overrideExisting) {
  const storageKey = `${sdkKey}_${productId}_global_properties`;
  let properties = {};

  // Try to load any existing properties
  const existing = localStorage.getItem(storageKey);
  if (existing) {
    try {
      properties = JSON.parse(existing) || {};
    } catch (e) {
      // If parsing fails, reset properties
      properties = {};
    }
  }

  // Only allow up to 5 keys
  const propertyExists = Object.prototype.hasOwnProperty.call(properties, propertyKey);
  const numKeys = Object.keys(properties).length;
  if (!propertyExists && numKeys >= 5) {
    console.warn("You can only set up to 5 Singular global properties.");
    return;
  }

  // Apply logic for overwrite or skip
  if (propertyExists && !overrideExisting) {
    console.log(`Global property '${propertyKey}' exists and overrideExisting is false; property not changed.`);
    return;
  }

  properties[propertyKey] = propertyValue;
  localStorage.setItem(storageKey, JSON.stringify(properties));
  console.log("Singular Global Properties set:", storageKey, properties);
}

有机搜索跟踪

有机搜索跟踪示例
#

重要!本示例为启用有机搜索跟踪的变通解决方案。代码只能作为示例使用,由网站开发人员根据营销部门的需要进行更新和维护。 有机搜索跟踪可能因广告商而异。 请查看示例并根据您的需要进行调整。

为何使用?

  • 确保正确跟踪有机搜索访问,即使没有广告系列参数。

  • 将Singular "来源 "参数wpsrc与(referrer)值和 "广告系列名称 "参数wpcn 作为 "OrganicSearch "附加到 URL,以明确归属。

  • 将当前 URL 和推荐人存储在localStorage中,以便日后使用。

  • 纯 JavaScript,零依赖性,易于集成。

工作原理

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

  2. 如果不存在营销活动参数,且推荐人是搜索引擎,则附加:

    • wpsrc (以推荐人作为其值
    • wpcn (以 OrganicSearch 作为其值)
  3. 更新浏览器中的 URL,无需重新加载页面。

  4. localStorage中的当前 URL 和推荐人存储为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(高级):手动设置奇异设备 ID

如果不想让 Singular SDK 自动保存设备 ID,可以跨域手动保存 ID,例如使用顶级域 cookie 或服务器端 cookie。该值应为 Singular 先前生成的 ID,格式为有效的 uuid4。

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

withPersistentSingularDeviceId 方法

说明

使用配置选项(包括要持久化的奇异设备 ID)初始化 SDK。

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

下一步

相关文章