概述
信息:Web Attribution 是一项企业功能。请联系您的客户成功经理,为您的账户启用该功能。
本指南介绍如何使用本地 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 AppbundleID相匹配。
-
它是什么?您网站的唯一名称,最好使用反向 DNS 格式(如
- 编辑网站 HTML 代码的权限。
- 在页面的
<head>部分添加 JavaScript 的权限。 - 要跟踪的事件列表。查看我们的Singular 标准事件:完整列表和按垂直领域推荐的事件,以了解相关想法。
实施步骤
第 1 步:添加 SDK 库脚本
将以下代码段添加到网站每个页面的<head> 部分。尽早放置,最好靠近<head> 标签的顶部。
提示!尽早添加脚本可确保页面源代码中的任何 Singular 函数都能使用 Singular JavaScript 库。
<script src="https://web-sdk-cdn.singular.net/singular-sdk/latest/singular-sdk.js"></script>
在 WebSDK JavaScript 库的路径中指定具体版本,例如:此处注明 1.4.3:
<script src="https://web-sdk-cdn.singular.net/singular-sdk/1.4.3/singular-sdk.js"></script>
-
在项目根目录下运行
npm i singular-sdk,或将"singular-sdk": "^1.4.3"添加到package.json文件的依赖项部分,然后运行npm install。 -
在要使用 SDK 的脚本中添加以下代码。
import {singularSdk, SingularConfig} from "singular-sdk";
与 Next.js / React 一起使用
Next.js是一个 React 框架,提供服务器端渲染(SSR)、静态网站生成(SSG)和 React 服务器组件。 由于 Singular WebSDK 需要浏览器 API(DOM、localStorage、cookie),因此必须仅在客户端加载。
重要提示:切勿在服务器端代码(如getServerSideProps 、React Server Components 或 Node.js 环境)中加载 Singular SDK。这会导致错误,因为服务器上无法使用浏览器 API。
方法 1:使用 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在浏览器空闲时加载--适用于非关键部件。
方法 2:组件级加载的动态导入
为实现组件级控制,可使用 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 的初始化过程中使用。
应用程序路由器与页面路由器
| 路由器类型 | 主要区别 | 脚本位置 |
|---|---|---|
| 应用程序路由器(Next.js 13+) |
组件默认为服务器组件。为浏览器 API 添加'use client' 指令。 |
app/layout.tsx
|
| 页面路由器(Next.js 12 及更早版本) | 所有组件默认为客户端组件。 |
pages/_app.tsx
|
第 2 步:初始化 SDK
- 每次在浏览器中加载页面时,始终初始化 SDK。
- 所有 Singular 归属和事件跟踪功能都需要初始化。
- 初始化会触发
__PAGE_VISIT__事件。 - 该
__PAGE_VISIT__事件用于在满足以下条件时在服务器端生成一个新的会话:- 用户到达时 URL 中包含新的广告数据(如 UTM 或 WP 参数),或
- 上一个会话已过期(30 分钟未活动后)。
- 会话用于衡量用户保留率并支持重新参与归因。
- 创建初始化函数,并在页面加载后的 DOM 就绪时调用该函数。
- 确保初始化发生在报告任何其他 Singular 事件之前。
- 对于单页面应用程序 (SPA),应在首次页面加载时初始化 Singular SDK,然后在代表新的页面视图的每次路由变化时调用 Singular Page Visist 函数
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 就绪后运行代码(例如,获取奇异设备 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 中的脚本组件时,添加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 就绪后运行代码(例如,获取奇异设备 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 证书。 -
对于 SPA,在路由更改时调用
window.singularSdk.pageVisit()以跟踪导航。 -
使用
useRef跟踪初始化状态,防止重复初始化。 -
在调用方法之前,为
window.singularSdk添加 null 检查,以防止在 SSR 期间出现错误。 - 在开发模式下进行测试,确保 SDK 正确初始化,不会出现服务器端错误。
Next.js 的开发人员检查表
- 验证步骤 1 中加载的脚本是否成功。
- 使用 SDK 凭据设置环境变量。
- 创建 TypeScript 类型声明,以获得更好的开发体验。
-
选择初始化方法:脚本组件
onLoad或useEffect钩子。 -
对于 SPA,请使用
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 初始化时调用回调函数 | 初始化完成时调用回调函数 |
开发人员核对表
- 收集 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 货币代码形式传递收入货币,如 "美元"、"欧元 "或 "印度卢比"。
- 将收入金额值转换为十进制值。
// 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事件监听器来检测表单提交情况,包括一个防止多重事件触发的机制(使用data-event-fired属性),并将带有自定义属性 (signup_method) 的事件发送到 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方法向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
- 以字符串形式发送,即使是数字:
// After user logs in or signs up
var userId = 'user_12345';
window.singularSdk.login(userId);
// After user logs out
window.singularSdk.logout();
步骤 5:防止重复事件
重要!这是最常见的实施错误之一。如果没有适当的防护措施,事件可能会多次触发,从而夸大您的指标。
在网络应用程序中防止重复收入事件对于准确分析和避免过度报告转换非常重要。我们的目标是确保每个唯一事件在每个会话或页面访问中只触发一次,即使用户重新加载了页面或无意中触发了多次提交。
- 每个事件都会生成一个唯一密钥(基于事件详细信息和可选自定义属性)。
- 该密钥存储在浏览器的会话存储(或用于持久重复数据删除的本地存储)中。
- 在发送之前,代码会检查是否已经触发了具有相同有效载荷的收入事件。
- 如果没有,就会发送事件并保存密钥。
- 如果是,则会阻止重复并通知用户或开发人员。
使用会话存储方法
// 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
- 在浏览器中打开网站
- 打开开发工具(F12 或右键单击 → 检查)
- 转到控制台选项卡
- 输入
typeof singularSdk并按 Enter - 您应该看到一个 "函数 "对象,而不是 "未定义 "对象
验证网络请求
- 打开 "开发工具"(F12)
- 转到网络选项卡
- 重新加载页面
- 按
singular或sdk-api过滤 - 查找指向
sdk-api-v1.singular.net的请求 - 单击请求查看详细信息
- 验证状态代码是否为
200 - 检查请求有效载荷是否包含您的产品 ID。这将出现在有效负载的 "
i"参数中。
成功!如果您看到sdk-api-v1.singular.net 请求的状态代码为 200,说明您的 SDK 已成功向 Singular 发送数据。
验证事件
- 在网站上触发一个事件(点击按钮、提交表单等)
- 在 "网络"选项卡中,查找指向
sdk-api-v1.singular.net的新请求 - 点击请求并查看有效载荷或请求选项卡
-
确认请求中出现了事件名称 "
n" 参数
使用测试控制台进行端到端测试
您可以使用 Singular 面板上的SDK 控制台测试 WebSDK集成。
- 在 Singular 平台中,转到开发工具 > 测试控制台。
- 单击 "添加设备"。
- 选择平台"Web"并输入Singular 设备 ID。
- 从浏览器有效载荷中获取 Singular 设备 ID。查看上面的截图,在事件上找到
SDID。 - 在单独窗口中进行测试时,测试控制台必须保持激活状态。控制台关闭或离线时不会显示任何触发的事件。
- 添加 SDID 后,就可以重新加载网页并触发一些事件。SDK 控制台将显示"__PAGE_VISIT___"和其他事件(如果触发)。
- 利用导出日志(报告和洞察导出日志)是验证测试结果的另一种方法。此数据集延迟 1 小时。
步骤 7:实施网络到应用程序转发
网络到应用程序归因转发
使用 Singular WebSDK 跟踪从网站到移动应用的用户旅程,从而将网络营销活动准确归因于移动应用的安装和重新吸引。按照以下步骤设置网站到应用程序的转发,包括为桌面用户提供 QR 码支持。
- 按照我们的《网站到移动应用归因转发指南》,为移动网络归因配置 Singular WebSDK。
- 对于桌面网络到应用程序跟踪:
- 完成上述指南中的设置。
- 使用 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 文件中运行该代码,也可以在您的网站中修改该代码,以便进行高级集成和故障排除。
将下面的代码复制并粘贴为 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 SDK 允许你定义自定义属性,这些属性将与应用程序发送的每个会话和事件一起发送到 Singular 服务器。这些属性可以代表有关用户、应用程序模式/状态或其他任何信息。
-
您最多可以将 5 个全局属性定义为有效的 JSON 对象。全局属性会被持久保存在浏览器
localstorage中,直到被清除或浏览器上下文发生变化。 -
每个属性名称和值的长度不超过 200 个字符。如果传递的属性名称或值较长,则会被截断为 200 个字符。
-
全局属性目前反映在 Singular 的用户级事件日志(请参阅导出属性日志)和回传中。
-
如果需要,全局属性可以在从 Singular 发送到第三方的回帖中进行匹配。
为支持在 WebSDK 初始化之前设置全局属性,您必须实现自定义setGlobalPropertyBeforeInit函数,并在 SDK 初始化之前调用该函数。如果不需要在初始化之前设置全局属性,则可以省略这段自定义代码。
要在初始化后处理全局属性,必须使用 SDK 函数: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);
}
/** 允许最多 5 个键/值对。可选择覆盖某个键的现有值。 * * @param {string} propertyKey - 要设置的属性键。
* @param {string} propertyValue - 要设置的属性值。 * @param {boolean} overrideExisting - 如果属性已经存在,是否覆盖该属性。 */ // Usage 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();
有机搜索跟踪
重要!本示例为启用有机搜索跟踪的变通解决方案。代码只能作为示例使用,由网站开发人员根据营销部门的需要进行更新和维护。 有机搜索跟踪可能因广告商而异。 请查看示例并根据您的需要进行调整。
为何使用?
-
确保正确跟踪有机搜索访问,即使没有广告系列参数。
-
将Singular "来源 "参数
wpsrc与(referrer)值和 "广告系列名称 "参数wpcn作为 "OrganicSearch "附加到 URL,以明确归属。 -
将当前 URL 和推荐人存储在
localStorage中,以便日后使用。 -
纯 JavaScript,零依赖性,易于集成。
工作原理
-
检查页面 URL 是否有来自(Google、Facebook、TikTok、UTM 等)的已知营销活动参数。
-
如果不存在营销活动参数,且推荐人是搜索引擎,则附加:
-
wpsrc(以推荐人作为其值 -
wpcn(以 OrganicSearch 作为其值)
-
-
更新浏览器中的 URL,无需重新加载页面。
-
将
localStorage中的当前 URL 和推荐人存储为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(高级):手动设置奇异设备 ID
如果不想让 Singular SDK 自动保存设备 ID,可以跨域手动保存 ID,例如使用顶级域 cookie 或服务器端 cookie。该值应为 Singular 先前生成的 ID,格式为有效的 uuid4。
注意:调用 init 方法或 InitFinishedCallback 后,可以使用 singularSdk.getSingularDeviceId() 读取 Singular 设备 ID。
| withPersistentSingularDeviceId 方法 | |
|---|---|
|
说明 |
使用配置选项(包括要持久化的奇异设备 ID)初始化 SDK。 |
| 签名 | withPersistentSingularDeviceId(singularDeviceId) |
| 使用示例 |
|
下一步
- 在 Singular 中为您的网络营销活动创建网站链接
- 如果您为移动库存开展网络营销活动,请遵循我们为Google Ads Web、Facebook Web 和TikTok Ads Web 提供的指南。
- 在Singular报告中监控数据