概述
INFO:Web归因是企业级功能。请联系您的客户成功经理为您的账户启用此功能。
本指南将指导您使用原生 JavaScript 实现 Singular WebSDK。此方法提供最可靠的跟踪效果,且不受常见广告拦截器阻挡,因此推荐用于大多数实施场景。
重要提示!
- 请勿同时采用原生 JavaScript 和 Google Tag Manager 两种方法,仅选择其中一种以避免重复追踪。
- Singular WebSDK专为用户浏览器端运行设计。其正常运作需访问浏览器功能( 如localStorage和 文档对象模型DOM)。请勿尝试在服务器端运行SDK(例如通过Next.js SSR或node.js),因服务器环境无法访问浏览器API,将导致追踪失败。
前提条件
开始前请确保您拥有:
-
SDK密钥与SDK密钥密钥:
- 获取路径:登录Singular账户,导航至开发者工具 > SDK集成 > SDK密钥。
-
产品ID:
-
定义:网站的唯一标识名,建议采用反向DNS格式(例如:
com.website-name)。 - 重要性说明:该ID将网站关联为Singular中的应用程序,必须与您在Singular应用页面中列出的Web应用程序bundleID完全一致。
-
定义:网站的唯一标识名,建议采用反向DNS格式(例如:
- 编辑网站HTML代码的权限。
- 在页面
<head>区域添加JavaScript的权限。 - 您希望追踪的事件列表。参考我们的《Singular标准事件:完整列表》及《垂直领域推荐事件》获取灵感。
实施步骤
步骤1:添加SDK库脚本
将以下代码片段添加至网站所有页面的<head> 部分。请尽可能置于页面顶部,理想位置为<head> 标签附近。
提示!提前添加脚本可确保页面源代码中的所有Singular函数都能调用JavaScript库。
<script src="https://web-sdk-cdn.singular.net/singular-sdk/latest/singular-sdk.js"></script>
在WebSDK JavaScript库路径中指定具体版本号,例如:1.4.8版本路径如下:
<script src="https://web-sdk-cdn.singular.net/singular-sdk/1.4.8/singular-sdk.js"></script>
-
在项目根目录运行
npm i singular-sdk, 或将"singular-sdk": "^1.4.8"添加至 package.json 文件的dependencies部分, 随后执行npm install。 -
在需要使用 SDK 的脚本中添加以下代码:
import {singularSdk, SingularConfig} from "singular-sdk";
Next.js / React 集成方案
Next.js作为 React 框架,提供服务器端渲染 (SSR)、静态网站生成 (SSG) 及 React 服务器组件功能。 由于 Singular WebSDK 需调用浏览器 API(DOM、localStorage、cookies),必须仅在客户端加载。
重要提示:切勿在服务器端代码(如getServerSideProps 、React Server Components或Node.js环境)中加载Singular SDK。此操作将导致错误,因为服务器端无法访问浏览器API。
方法一:使用Next.js脚本组件(推荐)
Next.js的<Script> 组件提供优化加载策略,
可防止路由切换时重复注入脚本。
// pages/_app.tsx (Pages Router) or app/layout.tsx (App Router)
import Script from 'next/script'
export default function App({ Component, pageProps }) {
return (
<>
<Script
src="https://web-sdk-cdn.singular.net/singular-sdk/latest/singular-sdk.js"
strategy="afterInteractive"
/>
<Component {...pageProps} />
</>
)
}
脚本策略选项:
-
afterInteractive(推荐):页面进入交互状态后加载——适用于分析和追踪脚本。 -
lazyOnload:在浏览器空闲时加载——适用于非关键组件。
方法二:组件级动态导入
若需组件级控制,可结合 Next.js 动态导入与ssr: false 实现按需加载:
// pages/index.tsx
import dynamic from 'next/dynamic'
const SingularSDKLoader = dynamic(
() => import('../components/SingularSDKLoader'),
{ ssr: false }
)
export default function Page() {
return (
<div>
<SingularSDKLoader />
<h1>Your Page Content</h1>
</div>
)
}
// components/SingularSDKLoader.tsx
'use client' // Required for Next.js App Router
import { useEffect } from 'react'
export default function SingularSDKLoader() {
useEffect(() => {
// Load Singular SDK script dynamically
const script = document.createElement('script')
script.src = 'https://web-sdk-cdn.singular.net/singular-sdk/latest/singular-sdk.js'
script.async = true
document.body.appendChild(script)
// Cleanup on unmount
return () => {
if (document.body.contains(script)) {
document.body.removeChild(script)
}
}
}, [])
return null
}
环境变量配置
提示!将Singular凭证存储在
以NEXT_PUBLIC_ 为前缀的环境变量中,
使其可在浏览器中使用:
# .env.local
NEXT_PUBLIC_SINGULAR_SDK_KEY=your_sdk_key_here
NEXT_PUBLIC_SINGULAR_SDK_SECRET=your_sdk_secret_here
NEXT_PUBLIC_SINGULAR_PRODUCT_ID=com.your-website
这些环境变量可在客户端代码中访问, 并用于步骤2的初始化过程。
应用路由器 vs 页面路由器
| 路由器类型 | 核心差异 | 脚本位置 |
|---|---|---|
| 应用路由器(Next.js 13+) |
组件默认为服务器端组件。需添加'use client' 指令才能使用浏览器API。
|
app/layout.tsx
|
| 页面路由器(Next.js 12 及更早版本) | 默认所有组件均为客户端组件。 |
pages/_app.tsx
|
步骤 2:初始化 SDK
- 每次页面在浏览器加载时都需初始化 SDK。
- 所有Singular归因和事件追踪功能均需初始化。
- 初始化将触发
__PAGE_VISIT__事件。 - 当满足以下条件时,
__PAGE_VISIT__事件用于在满足以下条件时在服务器端生成新会话:- 用户携带新广告数据(如UTM或WP参数)通过URL访问,或
- 前次会话已过期(30分钟无操作后)。
- 会话用于衡量用户留存率并支持重新参与归因分析。
- 创建初始化函数,并在页面加载完成后的DOM就绪阶段调用该函数。
- 确保在报告任何其他 Singular 事件之前完成初始化。
- 对于单页应用程序(SPA),请在首次页面加载时初始化Singular SDK,并在每次路由变更(代表新页面浏览)时调用Singular页面访问函数
window.singularSdk.pageVisit()。
基础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)路由的基本初始化
| 场景 | 操作步骤 |
|---|---|
|
首次页面加载 |
调用 |
|
导航至新路由/页面 |
调用 |
|
在SPA初始加载时 |
不调用 |
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();
});
Next.js / React 初始化
在步骤1加载SDK脚本后,待DOM就绪时进行初始化。初始化必须在脚本加载完成后于客户端执行。
TypeScript类型声明
创建类型声明文件以添加对 Singular SDK 的 TypeScript 支持:
// types/singular.d.ts
interface SingularConfig {
new (sdkKey: string, sdkSecret: string, productId: string): SingularConfig;
withCustomUserId(userId: string): SingularConfig;
withAutoPersistentSingularDeviceId(domain: string): SingularConfig;
withLogLevel(level: number): SingularConfig;
withSessionTimeoutInMinutes(timeout: number): SingularConfig;
withProductName(productName: string): SingularConfig;
withPersistentSingularDeviceId(singularDeviceId: string): SingularConfig;
withInitFinishedCallback(callback: (params: { singularDeviceId: string }) => void): SingularConfig;
}
interface SingularSDK {
init(config: SingularConfig): void;
event(eventName: string, attributes?: Record<string, any>): void;
conversionEvent(eventName: string): void;
revenue(eventName: string, currency: string, amount: number, attributes?: Record<string, any>): void;
login(userId: string): void;
logout(): void;
pageVisit(): void;
buildWebToAppLink(baseLink: string): string;
getSingularDeviceId(): string;
setGlobalProperties(key: string, value: string, override: boolean): void;
getGlobalProperties(): Record<string, string>;
clearGlobalProperties(): void;
}
interface Window {
singularSdk: SingularSDK;
SingularConfig: typeof SingularConfig;
}
使用Next.js脚本组件的基本初始化
使用步骤1中的Script组件时,添加onLoad回调以初始化SDK:
// pages/_app.tsx or app/layout.tsx
import Script from 'next/script'
export default function App({ Component, pageProps }) {
return (
<>
<Script
src="https://web-sdk-cdn.singular.net/singular-sdk/latest/singular-sdk.js"
strategy="afterInteractive"
onLoad={() => {
// Initialize Singular SDK after script loads
const config = new (window as any).SingularConfig(
process.env.NEXT_PUBLIC_SINGULAR_SDK_KEY,
process.env.NEXT_PUBLIC_SINGULAR_SDK_SECRET,
process.env.NEXT_PUBLIC_SINGULAR_PRODUCT_ID
);
window.singularSdk.init(config);
}}
/>
<Component {...pageProps} />
</>
)
}
使用useEffect钩子初始化
若采用动态导入方式或需更精细控制,请通过useEffect 初始化:
// components/SingularInitializer.tsx
'use client'
import { useEffect } from 'react'
export default function SingularInitializer() {
useEffect(() => {
// Wait for script to load, then initialize
const checkAndInit = () => {
if (window.singularSdk && window.SingularConfig) {
const config = new window.SingularConfig(
process.env.NEXT_PUBLIC_SINGULAR_SDK_KEY!,
process.env.NEXT_PUBLIC_SINGULAR_SDK_SECRET!,
process.env.NEXT_PUBLIC_SINGULAR_PRODUCT_ID!
);
window.singularSdk.init(config);
} else {
// Retry if SDK not loaded yet
setTimeout(checkAndInit, 100);
}
};
checkAndInit();
}, []); // Empty dependency - run once on mount
return null;
}
通过配置选项初始化
通过链式调用.with 方法启用额外功能,例如:
跨子域跟踪或自定义用户ID:
const config = new window.SingularConfig(
process.env.NEXT_PUBLIC_SINGULAR_SDK_KEY!,
process.env.NEXT_PUBLIC_SINGULAR_SDK_SECRET!,
process.env.NEXT_PUBLIC_SINGULAR_PRODUCT_ID!
)
.withAutoPersistentSingularDeviceId('your-domain.com')
.withCustomUserId(userId)
.withLogLevel(2); // 0=None, 1=Warn, 2=Info, 3=Debug
window.singularSdk.init(config);
单页应用(SPA)路由
对于采用客户端路由的Next.js应用,需将路由变更追踪与初始化分离处理。
重要提示:仅在首次页面加载时初始化SDK。
后续每次路由变更时调用window.singularSdk.pageVisit()。
// components/SingularPageTracker.tsx
'use client'
import { useEffect, useRef } from 'react'
import { usePathname } from 'next/navigation' // App Router
// or import { useRouter } from 'next/router' // Pages Router
export default function SingularPageTracker() {
const pathname = usePathname() // App Router
// const router = useRouter(); const pathname = router.pathname // Pages Router
const isInitialized = useRef(false);
// Initialize SDK once on mount
useEffect(() => {
if (!isInitialized.current && window.singularSdk && window.SingularConfig) {
const config = new window.SingularConfig(
process.env.NEXT_PUBLIC_SINGULAR_SDK_KEY!,
process.env.NEXT_PUBLIC_SINGULAR_SDK_SECRET!,
process.env.NEXT_PUBLIC_SINGULAR_PRODUCT_ID!
);
window.singularSdk.init(config);
isInitialized.current = true;
}
}, []); // Empty dependency - runs once
// Track page visits on route changes (NOT on initial mount)
useEffect(() => {
if (isInitialized.current && window.singularSdk) {
window.singularSdk.pageVisit();
}
}, [pathname]); // Runs when pathname changes
return null;
}
使用初始化回调
若需在 SDK 就绪后执行代码(例如获取
Singular 设备 ID),请使用.withInitFinishedCallback():
const config = new window.SingularConfig(
process.env.NEXT_PUBLIC_SINGULAR_SDK_KEY!,
process.env.NEXT_PUBLIC_SINGULAR_SDK_SECRET!,
process.env.NEXT_PUBLIC_SINGULAR_PRODUCT_ID!
)
.withInitFinishedCallback((initParams) => {
const singularDeviceId = initParams.singularDeviceId;
console.log('Singular Device ID:', singularDeviceId);
// Store for analytics or pass to other services
});
window.singularSdk.init(config);
Next.js 初始化最佳实践
- 仅在应用挂载时初始化SDK,而非每次路由变更时执行。
-
使用以
NEXT_PUBLIC_为前缀的环境变量存储SDK凭证。 -
对于单页应用,在路由变更时调用
window.singularSdk.pageVisit()以追踪导航状态。 -
使用
useRef追踪初始化状态,防止重复初始化。 -
在调用方法前添加
window.singularSdk的空值检查, 以防止服务器端渲染时出错。 - 在开发模式下测试,确保SDK正确初始化 且无服务器端错误。
Next.js 开发者检查清单
- 在步骤 1 中验证脚本是否成功加载。
- 使用您的 SDK 凭据设置环境变量。
- 创建 TypeScript 类型声明以提升开发体验。 选择初始化方式: - 脚本组件 `xml-ph-0000@deepl.internal` - 或 `xml-ph-0001@deepl.internal` 钩子
-
选择初始化方式:脚本组件
onLoad或useEffect钩子。 -
对于单页应用,使用
pageVisit()实现路由变更追踪。 -
通过控制台检查
__PAGE_VISIT__事件来测试初始化。
-
将
'sdkKey'替换为您的实际SDK密钥。 -
将
'sdkSecret'替换为您的实际SDK密钥。 -
将
'productId'替换为您的实际产品ID。格式应为:com.website-name,且需与Singular平台应用页面中的BundleID值一致。
配置选项
通过链式调用.with 方法增强 WebSDK 配置以启用额外功能。
例如:通过在Cookie中持久化Singular设备ID(SDID)实现跨子域跟踪,或为处于活跃登录状态的回访用户添加自定义用户ID:
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自动持久化 |
|
|
启用手动跨子域跟踪 | 手动设置唯一设备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:防止重复事件"。
基础事件追踪
通过有效 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);
转化事件追踪
追踪转化事件:
var eventName = 'sng_complete_registration';
window.singularSdk.conversionEvent(eventName);
收入事件追踪
通过包含收入信息的转化事件进行追踪,并可添加可选属性以丰富上下文:
- 将收入货币以三字母ISO 4217货币代码形式传递, 例如 "USD"、"EUR"或"INR"。
- 将收入金额值作为十进制数传递。
// Revenue event tracking
var revenueEventName = 'sng_ecommerce_purchase';
var revenueCurrency = 'USD';
var revenueAmount = 49.99;
window.singularSdk.revenue(revenueEventName, revenueCurrency, revenueAmount);
// Optional: With attributes for more context
var revenueEventName = 'sng_ecommerce_purchase';
var revenueCurrency = 'USD';
var revenueAmount = 49.99;
var attributes = {
key1: 'value1', // First custom attribute
key2: 'value2' // Second custom attribute
};
window.singularSdk.revenue(revenueEventName, revenueCurrency, revenueAmount, attributes);
常见事件实现模式
页面加载事件
页面加载事件追踪机制通过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);
});
按钮点击事件
按钮点击事件追踪机制通过JavaScript监控用户点击ID为checkout-button的按钮,
并利用Singular分析SDK触发checkout_started 事件进行数据追踪。 该代码采用click事件监听器检测按钮交互,包含防止重复触发事件的机制(通过data-event-fired 属性实现),并携带自定义属性(cart_value 和currency )将事件发送至Singular平台。此方案特别适用于电商分析场景,可精准追踪用户启动结账流程的时刻——通过确保每个页面会话仅触发一次事件,保障数据准确性。
/**
* Tracks a checkout started event with the Singular SDK when a button is clicked.
* @param {string} eventName - The name of the event to track (e.g., 'checkout_started').
* @param {Object} attributes - A JSON object with custom event attributes.
* @param {number} attributes.cart_value - The total value of the cart (e.g., 99.99).
* @param {string} attributes.currency - The currency of the cart value (e.g., 'USD').
*/
document.getElementById('checkout-button').addEventListener('click', function(e) {
// Prevent multiple fires
if (this.hasAttribute('data-event-fired')) {
return;
}
this.setAttribute('data-event-fired', 'true');
var eventName = 'checkout_started';
var attributes = {
cart_value: 99.99, // Total value of the cart
currency: 'USD' // Currency of the cart value
};
window.singularSdk.event(eventName, attributes);
});
表单提交事件
表单提交事件追踪机制通过JavaScript监控用户提交ID为signup-form 的表单,
并使用Singular SDK触发signup_completed 事件进行分析追踪。
该代码采用submit事件监听器检测表单提交,
包含防止多次触发的机制(使用xml-ph-0003@),
通过自定义属性(signup_method 和 )
将事件发送至Singular平台。 该代码采用 事件监听器检测表单提交,包含防止重复触发事件的机制(使用data-event-fired属性),并通过自定义属性( )向Singular平台发送事件。此方案适用于分析工作流中的用户注册追踪,例如监控用户获取或注册渠道,通过确保每个页面会话仅触发一次事件来保障数据准确性。
以下是简洁示例,将原始代码改为使用
window.singularSdk.event ,
并分离事件名称与属性以增强可读性。
/**
* Tracks a signup completed event with the Singular SDK when a form is submitted.
* @param {string} eventName - The name of the event to track (e.g., 'signup_completed').
* @param {Object} attributes - A JSON object with custom event attributes.
* @param {string} attributes.signup_method - The method used for signup (e.g., 'email').
* @example
* // Track a signup event with additional attributes
* var eventName = 'signup_completed';
* var attributes = {
* signup_method: 'email',
* email: document.getElementById('email')?.value || 'unknown'
* };
* window.singularSdk.event(eventName, attributes);
*/
document.getElementById('signup-form').addEventListener('submit', function(e) {
// Prevent multiple fires
if (this.hasAttribute('data-event-fired')) {
return;
}
this.setAttribute('data-event-fired', 'true');
var eventName = 'signup_completed';
var attributes = {
signup_method: 'email' // Method used for signup
};
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
- 即使为数字也应以字符串形式发送:
// After user logs in or signs up
var userId = 'user_12345';
window.singularSdk.login(userId);
// After user logs out
window.singularSdk.logout();
步骤5:事件去重(可选)
重要提示!若您的网站在短时间内可能多次触发相同事件,导出数据中可能出现重复事件。启用SDK去重功能可自动抑制重复事件。
Singular的Web SDK支持可选事件去重功能,可减少因短时间内重复触发导致的重复导出。启用后,SDK将在配置的时间窗口内剔除匹配相同去重参数的重复事件。
启用方法
-
withEventsDedupEnabled:启用事件去重(默认禁用)。 -
withTimeBetweenEvents: 判定事件重复的最大时间窗口(毫秒制);默认值为1000毫秒(1秒)。
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将根据事件的关键字段计算哈希值,并在时间窗口内抑制重复事件。去重参数包括:EventName 、EventProductName 、IsRevenueEvent 、CustomUserId 、GlobalProperties 、MatchId 及WebUrl (SDK在哈希计算时还会考虑额外事件数据,如收入/参数)。
步骤 6:测试实现
完成SDK集成后,请通过浏览器开发者工具验证其运行状态。
验证SDK加载状态
- 在浏览器中打开您的网站
- 打开开发者工具(F12 或右键点击 → 检查)
- 切换至控制台选项卡
- 输入
typeof singularSdk并按Enter键 - 应显示"function"对象而非"undefined"
验证网络请求
- 打开开发者工具(F12)
- 转到“网络”选项卡
- 重新加载页面
- 按
singular或sdk-api过滤 - 查找发往
sdk-api-v1.singular.net的请求 - 点击请求查看详情
- 验证状态码为
200 - 检查请求负载是否包含您的产品ID。该ID位于负载的"
i"参数中。
成功!若看到状态码为200的sdk-api-v1.singular.net 请求,说明您的SDK已成功向Singular发送数据。
验证事件
- 在您的网站上触发事件(点击按钮、提交表单等)
- 在"网络"选项卡中查找发往
sdk-api-v1.singular.net的新请求 - 点击该请求,查看"有效负载"或"请求"选项卡
-
确认请求中包含事件名称参数"
n"
进行端到端测试时,请使用测试控制台
您可通过Singular仪表盘中的SDK控制台测试Web SDK集成。
- 在Singular平台中,前往开发者工具 > 测试控制台。
- 点击添加设备。
- 选择平台"Web"并输入Singular设备ID。
- 从浏览器有效负载中获取Singular设备ID。请参照上方截图定位事件中的
SDID字段。 - 测试期间需保持测试控制台处于活动状态(可在独立窗口操作)。控制台关闭或离线时触发的事件将无法显示。
- 添加SDID后,可刷新网页并触发若干事件。SDK控制台将显示"__PAGE_VISIT__"及其他已触发的事件。
- 通过导出日志(报告与洞察导出日志)是验证测试结果的另一种方式。该数据集存在1小时延迟。
步骤7:实现网页到应用的转化追踪
网页到应用归因转发
使用Singular WebSDK追踪用户从网站到移动应用的旅程,实现精准的网页活动归因至应用安装及再互动。请按以下步骤配置网页到应用归因转发(含桌面端用户二维码支持)。
- 请参照《网站到移动应用归因转发指南》配置 Singular WebSDK 以实现移动网页归因。
- 桌面端网页到应用追踪:
- 完成上述指南中的配置步骤。
- 使用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文件中运行此代码,或将其集成至网站进行高级整合与故障排查。
将下方代码复制粘贴为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 SDK 允许您定义自定义属性,这些属性将随应用程序发送的每次会话和事件一同发送到 Singular 服务器。这些属性可代表您希望获取的任何信息,包括用户信息、应用程序模式/状态或其他任何内容。
-
最多可定义5个全局属性, 以有效JSON对象形式存储。 全局属性将保存在浏览器的
localstorage中, 直至被清除或浏览器上下文变更。 -
每个属性名称和值最长可达200个字符。 若传递的属性名称或值超出此限制, 将被截断为200个字符。
-
全局属性目前会反映在Singular的 用户级事件日志(参见《导出归因日志》) 及回传数据中。
-
全局属性可用于Singular向第三方发送回传数据时 进行匹配操作。
若要在WebSDK初始化阶段设置全局属性,
必须在配置对象中实现.withGlobalProperties()选项。
初始化后处理全局属性时,必须使用
SDK函数: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);
/**
* 在SDK初始化后设置单个全局属性。
* 允许最多5个键值对。可选覆盖现有键的值。
*
* @param {string} propertyKey - 要设置的属性键。
* @param {string} propertyValue - 要设置的属性值。
* @param {boolean} overrideExisting - 是否覆盖已存在的属性。
*/
// 使用示例
window.singularSdk.setGlobalProperties(propertyKey, propertyValue, overrideExisting);
// Get the JSON Object of global property values.
// Usage
window.singularSdk.getGlobalProperties();
// Clears all global property values.
// Usage
window.singularSdk.clearGlobalProperties();
自然搜索追踪
重要提示!本示例作为 临时解决方案 提供,用于启用自然搜索跟踪功能。该代码仅供参考, 应由 网站开发人员根据营销部门需求进行更新维护。 自然搜索 跟踪对不同广告主可能具有不同含义。 请 审阅示例并根据实际需求调整。
为何使用此方案?
-
确保自然搜索访问被正确追踪, 即使未携带广告系列参数。
-
在URL末尾添加Singular形式的"来源"参数
wpsrc(取值自推荐来源),并添加"活动名称"参数wpcn(取值设为"OrganicSearch"), 实现清晰归因。 -
将当前URL和来源网址存储在
localStorage中 供后续使用。 -
纯 JavaScript 实现,零依赖,轻松集成。
工作原理
-
检查页面URL是否包含已知营销活动参数 (来自Google、Facebook、TikTok、UTM等)。
-
若未检测到营销参数且来源为搜索引擎,则追加:
-
wpsrc(将来源网址设为其值) -
wpcn(值为OrganicSearch)
-
-
更新浏览器中的URL,无需重新加载页面。
-
将当前URL和引荐来源存储在
localStorage中:sng_url和sng_ref。
使用方法
-
将
setupOrganicSearchTracking.js作为库添加到 您的网站中。 -
在初始化WebSDK之前
调用
setupOrganicSearchTracking()函数。
// 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(高级):手动设置 Singular 设备 ID
若不希望 Singular SDK 自动持久化设备 ID, 可通过 跨域手动保存 ID(例如使用 顶级域 Cookie 或服务器端 Cookie)。该值应为 Singular 先前 生成的 有效 uuid4 格式 ID。
注意:调用init方法后或通过InitFinishedCallback回调, 可使用singularSdk.getSingularDeviceId()读取Singular设备ID。
| withPersistentSingularDeviceId 方法 | |
|---|---|
|
描述 |
使用包含需持久化的Singular设备ID的配置选项初始化SDK。 |
| 签名 | withPersistentSingularDeviceId(singularDeviceId) |
| 用法示例 |
|
后续步骤
- 在Singular中为您的网络广告活动创建网站链接
- 若您在移动端投放网络广告,请遵循我们的指南操作Google Ads网络版、Facebook网络版及 TikTok Ads网络版。
- 通过Singular报告监控数据