개요
참고: 웹 어트리뷰션은 엔터프라이즈 기능입니다. 계정에 이 기능을 활성화하려면 고객 성공 매니저에게 문의하십시오.
이 가이드는 네이티브 JavaScript를 사용하여 Singular WebSDK를 구현하는 방법을 안내합니다. 이 방법은 가장 안정적인 추적을 제공하며, 일반적인 광고 차단기에 의해 차단되지 않으므로 대부분의 구현에 권장됩니다.
중요!
- 네이티브 자바Script와 Google 태그 관리자 방식을 동시에 구현하지 마십시오. 중복 추적을 방지하기 위해 하나만 선택하십시오.
- Singular 웹 SDK는 사용자의 브라우저에서 클라이언트 측으로 실행되도록 설계되었습니다. 정상적인 작동을 위해서는 localStorage 및 DOM(Document Object Model) 과 같은 브라우저 기능에 대한 접근이 필요합니다. 서버 측 (예: Next.js SSR 또는 node.js를 통한)에서 SDK를 실행하려고 시도하지 마십시오. 서버 환경은 브라우저 API에 대한 접근을 제공하지 않으므로 추적 오류가 발생합니다.
필수 조건
시작하기 전에 다음을 준비하십시오:
-
SDK 키 및 SDK 시크릿:
- 찾는 방법: Singular 계정에 로그인한 후 개발자 도구 > SDK 연동 > SDK 키로 이동하세요.
-
제품 ID:
-
정의: 웹사이트의 고유 이름으로, 역방향 DNS 형식(예:
com.website-name)을 사용하는 것이 이상적입니다. - 중요성: 이 ID는 웹사이트를 Singular 내 앱으로 연결하며, Singular의 앱 페이지에 기재된 웹 앱 bundleID와 반드시 일치해야 합니다.
-
정의: 웹사이트의 고유 이름으로, 역방향 DNS 형식(예:
- 웹사이트 HTML 코드 편집 권한.
- 페이지의
<head>섹션에 자바Script 추가 권한. - 추적하려는 이벤트 목록. 아이디어를 얻으려면 Singular 표준 이벤트: 전체 목록 및 업종별 추천 이벤트를 참조하세요.
구현 단계
1단계: SDK 라이브러리 Script 추가
웹사이트의 모든 페이지 <head> 섹션에 다음 코드 스니펫을 추가하세요. 가능한 한 상단에 배치하며, 이상적으로는 <head> 태그 근처에 위치시켜야 합니다.
팁! Script를 조기에 추가하면 페이지 소스 코드의 모든 Singular 기능에 Singular 자바Script 라이브러리가 사용 가능해집니다.
<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를 실행하거나, package.json 파일의 dependencies 섹션에"singular-sdk": "^1.4.8"를 추가한 후npm install를 실행하세요. -
SDK를 사용하려는 Script에 다음 코드를 추가하세요.
import {singularSdk, SingularConfig} from "singular-sdk";
Next.js / React와 함께 사용하기
Next.js는 서버 측 렌더링(SSR), 정적 사이트 생성(SSG), React 서버 컴포넌트를 제공하는 React 프레임워크입니다. Singular WebSDK는 브라우저 API(DOM, localStorage, 쿠키)를 필요로 하므로 반드시 클라이언트 측에서만 로드해야 합니다.
중요: Singular SDK를 서버 측 코드(예: getServerSideProps, React Server Components 또는 Node.js 환경)에 절대 로드하지 마십시오. 서버에서는 브라우저 API를 사용할 수 없으므로 오류가 발생합니다.
방법 1: Next.js Script 컴포넌트 사용 (권장)
Next.js의 <Script> 컴포넌트는 최적화된 로딩 전략을 제공하며
경로 변경 시 중복 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} />
</>
)
}
Script 로딩 전략 옵션:
-
afterInteractive(권장): 페이지가 상호작용 가능해진 후 로드 - 분석 및 추적에 이상적입니다. -
lazyOnload: 브라우저 유휴 시간 동안 로드 - 중요하지 않은 위젯에 사용하세요.
방법 2: 컴포넌트 수준 로딩을 위한 동적 임포트
구성 요소 수준 제어를 위해 Next.js 동적 임포트를 ssr: false 와 함께 사용하여 필요한 경우에만 Script를 로드하세요:
// 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 페이지 라우터
| 라우터 유형 | 주요 차이점 | Script 위치 |
|---|---|---|
| 앱 라우터 (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 Ready 시점에 이 함수를 호출하십시오.
- 다른 Singular 이벤트가 보고되기 전에 초기화가 이루어지도록 하십시오.
-
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(Single-Page Application) 라우팅을 통한 기본 초기화
| 시나리오 | 처리 방법 |
|---|---|
|
첫 페이지 로드 |
|
|
새로운 경로/페이지로 이동 |
|
|
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 Script를 로드한 후, DOM이 준비되면 초기화하십시오. 초기화는 Script 로드 후 클라이언트 측에서 수행되어야 합니다.
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 Script 컴포넌트를 사용한 기본 초기화
단계 1의 Script 컴포넌트를 사용할 때 SDK를 초기화하기 위해 onLoad콜백을 추가하세요:
// 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);
Singular 페이지 애플리케이션(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는 애플리케이션 마운트 시 단 한 번만 초기화하고, 경로 변경 시마다 초기화하지 마십시오.
-
SDK 자격 증명에
NEXT_PUBLIC_접두사가 붙은 환경 변수를 사용하십시오. -
SPA의 경우, 네비게이션 추적을 위해 경로 변경 시
window.singularSdk.pageVisit()호출. -
useRef를 사용하여 초기화 상태를 추적하고 중복 초기화를 방지하십시오. -
SSR 중 오류를 방지하려면 메서드 호출 전에
window.singularSdk에 대한 null 검사를 추가하십시오. - 서버 측 오류 없이 SDK가 올바르게 초기화되는지 확인하려면 개발 모드에서 테스트하십시오.
Next.js 개발자 체크리스트
- 1단계에서 Script가 성공적으로 로드되었는지 확인하세요.
- SDK 자격 증명을 사용하여 환경 변수를 설정하세요.
- 개발자 경험을 향상시키기 위해 TypeScript 타입 선언을 생성하십시오. 초기화 방법 선택: Script 컴포넌트 xml-ph-0000@deepl.internal 또는 xml-ph-0001@deepl.internal 훅.
-
초기화 방법 선택: Script 컴포넌트(
onLoad) 또는useEffect훅. -
SPA의 경우
pageVisit()로 경로 변경 추적을 구현하세요. -
콘솔에서
__PAGE_VISIT__이벤트를 확인하여 초기화를 테스트하세요.
-
실제 SDK 키로
'sdkKey'실제 SDK 키로 교체하십시오. -
실제 SDK 시크릿으로
'sdkSecret'실제 SDK 비밀 키로 교체하십시오. -
Replace
'productId'실제 제품 ID로 대체하십시오. 형식은 다음과 같아야 합니다:com.website-name그리고 Singular 플랫폼의 앱 페이지에 있는 번들 ID 값과 일치해야 합니다.
구성 옵션
.with 메서드를 연결하여 추가 기능을 활성화함으로써 WebSDK 설정을 강화하십시오.
예를 들어, 쿠키에 Singular Device ID(SDID)를 저장하여 크로스 서브도메인 추적을 지원하거나, 활성 로그인 상태의 재방문 사이트 방문자를 위한 사용자 지정 ID를 포함하려면:
var domain = 'website-name.com';
var config = new SingularConfig('sdkKey','sdkSecret','productId')
.withAutoPersistentSingularDeviceId(domain)
.withCustomUserId(userId);
SingularConfig 메서드 참조
사용 가능한 모든 ".with" 메서드는 다음과 같습니다.
| 메소드 | 설명 | 자세히 보기 |
.withCustomUserId(customId)
|
사용자 ID를 Singular로 전송 | 사용자 ID 설정 |
.withProductName(productName)
|
제품에 대한 선택적 표시 이름 | |
.withLogLevel(logLevel)
|
로깅 수준 구성: 0 - 없음(기본값); 1 - 중요; 2 - 정보; 3 - 디버그. | |
.withSessionTimeoutInMinutes(timeout)
|
세션 시간 초과를 분 단위로 설정 (기본값: 30분) |
|
.withAutoPersistentSingularDeviceId(domain)
|
자동 크로스 서브도메인 추적 활성화 | 쿠키를 사용한 자동 지속 |
|
|
수동 크로스-서브도메인 추적 활성화 | Singular 기기 ID 수동 설정 |
.withInitFinishedCallback(callback)
|
SDK 초기화 완료 시 콜백 호출 | 초기화 완료 시 콜백 함수 호출 |
.withGlobalProperties(globalProperties, false)
|
초기화 중 글로벌 속성 설정; 두 번째 매개변수는
overrideExisting (부울)입니다.
false 로 기존 속성과 병합하고,
true 로 기존 속성을 덮어씁니다.
|
글로벌 속성 |
.withEventsDedupEnabled()
|
선택적 이벤트 중복 제거를 활성화하여 중복 이벤트 내보내기를 줄입니다 (예: 짧은 시간 내에 반복적으로 발동된 트리거) | 이벤트 중복제거 |
.withTimeBetweenEvents(timeBetweenEvents)
|
중복 제거를 위한 최대 시간 창(ms) 설정(기본값: 1000ms / 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);
구매 이벤트 추적
구매 정보가 포함된 전환 이벤트를 추적하고 추가 컨텍스트를 위한 선택적 속성을 추가하세요:
- 구매 통화는 3자리 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, 이미지, Script)가 로드된 시점을 감지합니다.
이 이벤트 발생 시 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 메서드를 사용하여 내부 사용자 ID를 Singular로 전송할 수 있습니다.
참고: 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() ` 메서드를 호출하십시오.
참고: 여러 사용자가 Singular 기기를 사용하는 경우, 로그인 및 로그아웃 시마다 사용자 ID를 설정 및 해제하는 로그아웃 절차를 구현하는 것이 좋습니다.
웹사이트에서 Singular SDK를 초기화할 때 사용자 ID를 이미 알고 있다면,
config 객체에 사용자 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의 웹 SDK는 짧은 시간 내에 반복적으로 발동되는 트리거로 인한 중복 내보내기를 줄이기 위해 선택적 이벤트 중복 제거를 지원합니다. 활성화 시, SDK는 설정된 시간 창 내에서 동일한 중복 제거 매개변수와 일치하는 중복 이벤트를 제거합니다.
활성화 방법
-
withEventsDedupEnabled: 이벤트 중복 제거 활성화 (기본값은 비활성화됨). -
withTimeBetweenEvents: 두 이벤트를 중복으로 간주하는 최대 시간 창(밀리초 단위); 기본값은 1000ms(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는 해싱 시 구매/인수(args)와 같은 추가 이벤트 "추가" 페이로드도 고려합니다).
6단계: 구현 테스트
SDK 구현 후 브라우저 개발자 도구를 사용하여 정상 작동 여부를 확인하십시오.
SDK 로드 확인
- 브라우저에서 웹사이트 열기
- 개발자 도구 열기(F12 또는 마우스 오른쪽 버튼 클릭 → 검사)
- 콘솔 탭으로 이동
-
typeof singularSdk를 입력하고 Enter 키를 누르세요 - "undefined"가 아닌 "function" 객체가 표시되어야 합니다
네트워크 요청 확인
- 개발자 도구 열기 (F12)
- 네트워크 탭으로 이동하세요
- 페이지를 다시 로드하세요
-
singular또는sdk-api로 필터링하세요 -
sdk-api-v1.singular.net로 전송된 요청 찾기 - 요청을 클릭하여 세부 정보 보기
-
상태 코드가
200인지 확인하세요 - 요청 페이로드에 제품 ID가 포함되어 있는지 확인하세요. 이는 페이로드의 "
i" 매개변수에 포함됩니다.
성공! 상태 코드 200으로 sdk-api-v1.singular.net 에 대한 요청이 보인다면, SDK가 Singular로 데이터를 성공적으로 전송하고 있는 것입니다.
이벤트 확인
- 웹사이트에서 이벤트를 트리거하세요(버튼 클릭, 양식 제출 등).
-
네트워크 탭에서
sdk-api-v1.singular.net로의 새 요청을 찾으세요. - 요청을 클릭하고 페이로드 또는 요청 탭을 확인하세요.
-
요청에 이벤트 이름 "
n" 매개변수가 표시되는지 확인하세요
종단 간 테스트를 위해서는 테스트 콘솔을 사용하십시오
Singular 대시보드에서 제공되는 SDK 콘솔을 사용하여 웹 SDK 연동을 테스트할 수 있습니다.
- Singular 플랫폼에서 개발자 도구 > 테스트 콘솔로 이동하십시오.
- 장치 추가를 클릭하십시오.
- 플랫폼 "웹"을 선택하고 Singular 기기 ID를 입력하세요.
- 브라우저 페이로드에서 Singular 장치 ID를 확인하세요. 이벤트에서
SDID를 찾는 방법은 위 스크린샷을 참조하세요. - 테스트 콘솔은 별도의 창에서 테스트하는 동안 활성 상태를 유지해야 합니다. 콘솔이 닫히거나 오프라인 상태일 때 발생하는 이벤트는 표시되지 않습니다.
- SDID를 추가한 후 웹페이지를 재로드하고 이벤트를 트리거하세요. "__PAGE_VISIT__" 및 기타 이벤트(트리거된 경우)가 SDK 콘솔에 표시됩니다.
- 테스트 결과를 확인하는 또 다른 방법은 로그 내보내기 (보고서 및 인사이트 로그 내보내기)를 활용하는 것입니다. 이 데이터 세트는 1시간 지연됩니다.
7단계: 웹-앱 전달 구현
웹-앱 어트리뷰션 전달
Singular WebSDK를 사용하여 웹사이트에서 모바일 앱으로의 사용자 여정을 추적하여 웹 캠페인에 대한 모바일 앱 설치 및 리인게이지먼트에 대한 정확한 어트리뷰션을 가능하게 합니다. 데스크톱 사용자를 위한 QR 코드 지원을 포함하여 웹-앱 전달을 설정하려면 다음 단계를 따르십시오.
- 모바일 웹 어트리뷰션용 Singular WebSDK 구성은 웹사이트-모바일 앱 어트리뷰션 전달 가이드를 참조하십시오.
- 데스크톱 웹-앱 추적의 경우:
- 상기 가이드의 설정을 완료하세요.
- Singular 모바일 웹-앱 링크(예:
https://yourlink.sng.link/...)와 QRCode.js 같은 동적 QR 코드 라이브러리를 사용한 WebSDK 함수buildWebToAppLink(link)를 활용하세요. - 웹페이지에 링크를 인코딩한 QR 코드를 생성하세요. 데스크톱 사용자는 모바일 기기로 이를 스캔하여 앱을 열고, 어트리뷰션용 캠페인 데이터를 전달할 수 있습니다.
- QR 코드 생성 및 웹-투-앱 링크 처리의 작동 예시는 Singular 웹SDK 데모에서 확인하세요.
팁! 모바일 인앱 브라우저 웹 뷰(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/ko/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 서버로 전송될 사용자 정의 속성을 정의할 수 있습니다. 이러한 속성은 사용자, 앱 모드/상태 또는 기타 원하는 모든 정보를 표현할 수 있습니다.
-
유효한 JSON 객체로 최대 5개의 글로벌 속성을 정의할 수 있습니다. 글로벌 속성은 브라우저의
localstorage에 브라우저 컨텍스트가 변경되거나 캐시가 지워질 때까지 저장됩니다. -
각 속성 이름과 값은 최대 200자까지 가능합니다. 더 긴 속성 이름이나 값을 전달하면 200자로 잘립니다.
-
글로벌 속성은 현재 Singular의 사용자 수준 이벤트 로그( '속성 로그 내보내기' 참조) 및 포스트백에 반영됩니다.
-
글로벌 속성은 매칭이 필요한 경우 Singular에서 제3자로 전송되는 포스트백에서 사용할 수 있습니다.
웹SDK 초기화 시 글로벌 속성 설정을 지원하려면
config 객체에 ` .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 초기화 후 Singular 글로벌 속성을 설정합니다.
* 최대 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형 "Source" 매개변수
wpsrc및 "Campaign Name" 매개변수wpcn을 "OrganicSearch"로 추가합니다. -
나중에 사용할 수 있도록 현재 URL과 리퍼러를
localStorage에 저장합니다. -
순수 자바Script, 제로 의존성, 쉬운 연동.
작동 방식
-
페이지 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 Device ID를 생성하고 브라우저 스토리지를 사용하여 이를 유지합니다. 이 스토리지는 서브도메인 간에 공유될 수 없기 때문에, SDK는 각 서브도메인마다 새로운 ID를 생성하게 됩니다.
서브도메인 간에 Singular Device ID를 지속하려면 다음 옵션 중 하나를 사용할 수 있습니다:
방법 B (고급): Singular 기기 ID 수동 설정
Singular SDK가 기기 ID를 자동으로 저장하지 않도록 하려면, 도메인 간에 ID를 수동으로 저장할 수 있습니다. 예를 들어, 최상위 도메인 쿠키나 서버 측 쿠키를 사용하세요. 값은 Singular에서 이전에 생성된 유효한 uuid4 형식의 ID여야 합니다.
참고: Singular Device ID는 init 메서드 호출 후 또는 InitFinishedCallback을 사용하여 singularSdk.getSingularDeviceId()로 읽을 수 있습니다.
| withPersistentSingularDeviceId 메서드 | |
|---|---|
|
설명 |
지속적으로 유지하려는 Singular Device Id를 포함한 구성 옵션으로 SDK를 초기화합니다. |
| 서명 | withPersistentSingularDeviceId(singularDeviceId) |
| 사용 예 |
|
다음 단계
- 웹 캠페인용 Singular 웹사이트 링크 생성
- 모바일 인벤토리를 위한 웹 캠페인을 운영하는 경우 Google Ads 웹, Facebook 웹 및 TikTok Ads 웹에 대한 가이드를 따르세요.
- Singular 보고서에서 데이터를 모니터링하세요