개요
INFO: 웹 어트리뷰션은 엔터프라이즈 기능입니다. 계정에 이 기능을 사용하려면 고객 성공 매니저에게 문의하세요.
이 가이드는 네이티브 자바Script를 사용하여 Singular WebSDK를 구현하는 방법을 안내합니다. 이 방법은 가장 안정적인 추적 기능을 제공하며 일반적인 광고 차단기에 의해 차단되지 않으므로 대부분의 구현에 권장됩니다.
중요!
- 네이티브 자바Script와 구글 태그 관리자 방법을 모두 구현하지 마세요. 중복 추적을 피하려면 하나만 선택하세요.
- Singular 웹SDK는 사용자의 브라우저에서 클라이언트 측에서 실행되도록 설계되었습니다. 제대로 작동하려면 localStorage 및 DOM(문서 객체 모델) 과 같은 브라우저 기능에 액세스할 수 있어야 합니다. 서버 환경에서는 브라우저 API에 대한 액세스를 제공하지 않으므로 추적에 실패할 수 있으므로 SDK를 서버 측 (예: Next.js SSR 또는 node.js)에서 실행하려고 시도하지 마세요.
전제 조건
시작하기 전에 다음이 필요한지 확인하세요:
-
SDK 키 및 SDK 시크릿:
- 어디서 찾을 수 있나요? Singular 계정에 로그인하고 개발자 도구 > SDK 연동 > SDK 키로 이동합니다.
-
제품 ID:
-
정의: 웹사이트의 고유한 이름으로, 역방향 DNS 형식을 사용하는 것이 이상적입니다(예:
com.website-name). - 중요한 이유: 이 ID는 웹사이트를 Singular 앱으로 연결하며 Singular 앱 페이지에 나열된 웹앱 번들ID와 일치해야 합니다.
-
정의: 웹사이트의 고유한 이름으로, 역방향 DNS 형식을 사용하는 것이 이상적입니다(예:
- 웹사이트의 HTML 코드를 편집할 수 있는 권한.
- 페이지의
<head>섹션에 JavaScript를 추가할 수 있는 액세스 권한. - 추적하려는 이벤트 목록. 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.3이 표시되어 있습니다):
<script src="https://web-sdk-cdn.singular.net/singular-sdk/1.4.3/singular-sdk.js"></script>
-
프로젝트의 루트 디렉터리에서
npm i singular-sdk을 실행하거나 package.json 파일의 종속성 섹션에"singular-sdk": "^1.4.3"을 추가한 다음npm install을 실행합니다. -
SDK를 사용하려는 Script에 다음 코드를 추가합니다.
import {singularSdk, SingularConfig} from "singular-sdk";
Next.js / React와 함께 사용
Next.js는 서버 측 렌더링(SSR), 정적 사이트 생성(SSG) 및 React 서버 컴포넌트를 제공하는 React 프레임워크입니다. Singular WebSDK에는 브라우저 API(DOM, localStorage, 쿠키)가 필요하므로 클라이언트 측에서만 로드해야 합니다.
중요: 서버 측 코드(예: getServerSideProps, React 서버 컴포넌트 또는 Node.js 환경)에서 Singular SDK를 로드하지 마세요. 서버에서 브라우저 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: 컴포넌트 수준 로딩을 위한 동적 가져오기
컴포넌트 수준 제어의 경우 ssr: false 을 사용하여 Next.js 동적 가져오기를 사용하여 필요할 때만 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단계의 초기화 중에 사용할 수 있습니다.
앱 라우터와 페이지 라우터 비교
| 라우터 유형 | 주요 차이점 | 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에서 이 함수를 호출합니다.
- 다른 특이 이벤트가 보고되기 전에 초기화가 이루어지도록 합니다.
-
SPA(Singular 페이지 애플리케이션)의 경우, 첫 페이지 로드 시 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();
});
Singular 페이지 애플리케이션(SPA) 라우팅을 사용한 기본 초기화
| 시나리오 | 수행할 작업 |
|---|---|
|
첫 페이지 로드 |
|
|
새 경로/페이지로 이동 |
|
|
SPA에서 초기 로드 시 |
|
SPA(React) 예제
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
/**
* Initializes the Singular SDK with the provided configuration.
* @param {string} sdkKey - The SDK key for Singular.
* @param {string} sdkSecret - The SDK secret for Singular.
* @param {string} productId - The product ID for Singular.
* @example
* // Initialize the Singular SDK
* initSingularSDK();
*/
function initSingularSDK() {
var config = new SingularConfig('sdkKey', 'sdkSecret', 'productId');
window.singularSdk.init(config);
}
/**
* Tracks a page visit event with the Singular SDK on route changes.
* @example
* // Track a page visit
* trackPageVisit();
*/
function trackPageVisit() {
window.singularSdk.pageVisit();
}
/**
* A React component that initializes the Singular SDK on mount and tracks page visits on route changes.
* @returns {JSX.Element} The component rendering the SPA content.
* @example
* // Use in a React SPA with react-router-dom
* function App() {
* const location = useLocation();
* useEffect(() => {
* initSingularSDK();
* }, []);
* useEffect(() => {
* trackPageVisit();
* }, [location.pathname]);
* return <div>Your SPA Content</div>;
* }
*/
function App() {
const location = useLocation();
useEffect(() => {
initSingularSDK();
}, []); // Run once on mount for SDK initialization
useEffect(() => {
trackPageVisit();
}, [location.pathname]); // Run on route changes for page visits
return (
<div>Your SPA Content</div>
);
}
export default App;
초기화 콜백을 사용한 기본 초기화
SDK가 준비된 후 코드를 실행해야 하는 경우(예: Singular 디바이스 ID를 가져오는 경우) .withInitFinishedCallback() 로 콜백을 설정합니다:
/**
* Initializes the Singular SDK with a callback to handle initialization completion.
* @param {string} sdkKey - The SDK key for Singular.
* @param {string} sdkSecret - The SDK secret for Singular.
* @param {string} productId - The product ID for Singular.
* @example
* initSingularSDK(); // Initializes SDK and logs device ID
*/
function initSingularSDK() {
var config = new SingularConfig('sdkKey', 'sdkSecret', 'productId')
.withInitFinishedCallback(function(initParams) {
var singularDeviceId = initParams.singularDeviceId;
// Example: Store device ID for analytics
console.log('Singular Device ID:', singularDeviceId);
// Optionally store in localStorage or use for event tracking
// localStorage.setItem('singularDeviceId', singularDeviceId);
});
window.singularSdk.init(config);
}
/**
* Triggers Singular SDK initialization when the DOM is fully parsed.
*/
document.addEventListener('DOMContentLoaded', function() {
initSingularSDK();
});
Next.js / React 초기화
1단계에서 SDK Script를 로드한 후 DOM이 준비되면 초기화합니다. 초기화는 Script가 로드된 후 클라이언트 측에서 수행해야 합니다.
타입Script 타입 선언
타입 선언 파일을 생성하여 Singular SDK에 대한 타입Script 지원을 추가합니다:
// 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 컴포넌트를 사용하는 경우 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 를 사용하여 초기화합니다:
// 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을 사용하여 초기화 상태를 추적하고 중복 초기화를 방지하세요. -
메소드를 호출하기 전에
window.singularSdk에 대한 null 검사를 추가하여 SSR 중 오류를 방지하세요. - 개발 모드에서 테스트하여 SDK가 서버 측 오류 없이 올바르게 초기화되는지 확인합니다.
Next.js에 대한 개발자 체크리스트
- 1단계에서 Script가 성공적으로 로드되었는지 확인합니다.
- SDK 자격 증명으로 환경 변수를 설정합니다.
- 더 나은 개발자 경험을 위해 타입Script 타입 선언을 생성합니다.
-
초기화 방법을 선택합니다: Script 컴포넌트
onLoad또는useEffect후크. -
SPA의 경우
pageVisit()로 경로 변경 추적을 구현합니다. -
콘솔에서
__PAGE_VISIT__이벤트를 확인하여 초기화를 테스트합니다.
-
Replace
'sdkKey'를 실제 SDK 키로 바꿉니다. -
Replace
'sdkSecret'를 실제 SDK 시크릿으로 바꿉니다. -
Replace
'productId'를 실제 제품 ID로 바꿉니다.com.website-name와 같이 표시되어야 하며, Singular 플랫폼의 앱 페이지에 있는 BundleID 값과 일치해야 합니다.
구성 옵션
.with 메소드를 연결하여 추가 기능을 사용하도록 WebSDK 설정을 개선합니다.
예를 들어, 쿠키에 SDID(Singular 기기 ID)를 유지하여 교차 하위 도메인 추적을 지원하거나 활성 로그인 상태의 재방문자를 위한 사용자 지정 사용자 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 초기화가 완료되면 콜백 호출하기 | 초기화가 완료되면 콜백 함수 호출하기 |
개발자 체크리스트
- SDK 자격 증명과 제품 ID를 수집합니다.
- 사용자 지정 설정(사용자 ID, 타임아웃 등)이 필요한지 결정합니다.
- 위의 예시를 사용하여 Singular 초기화 함수와 SingularConfig 객체를 생성합니다.
- 항상 페이지 로드 시 초기화가 한 번만 트리거되는지 테스트하세요.
팁! 자연 검색추적과 같은 고급 설정의 경우, 쿼리 매개변수 조정과 같은 사용자 지정 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);
구매 이벤트 추적
구매 정보로 전환 이벤트를 추적하고 추가 컨텍스트를 위해 선택적 속성을 추가하세요:
- 구매 통화를 'USD', 'EUR' 또는 'INR'과 같은 세 글자 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, 이미지, 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() 메소드를 사용합니다. 설정 해제하려면(예: 사용자가 계정에서 '로그아웃'하는 경우) 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" 파라미터에 있습니다.
성공! 상태 코드가 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 웹SDK를 구성하세요.
- 데스크톱 웹-투-앱 추적의 경우:
- 위 가이드의 설정을 완료합니다.
- Singular 모바일 웹-투-앱 링크(예:
https://yourlink.sng.link/...)와 WebSDK 함수(buildWebToAppLink(link))를 QRCode.js와 같은 동적 QR코드 라이브러리와 함께 사용합니다. - 웹페이지에 링크를 인코딩하는 QR 코드를 생성합니다. 데스크톱 사용자는 모바일 디바이스로 스캔하여 앱을 열고 어트리뷰션을 위한 캠페인 데이터를 전달할 수 있습니다.
- QR코드 생성 및 웹-투-앱 링크 처리의 작동 예시는 Singular 웹SDK 데모를 살펴보세요.
팁! 모바일 인앱 브라우저 웹 뷰(예: 페이스북, 인스타그램, 틱톡)는 사용자가 디바이스의 기본 브라우저로 이동하면 Singular 디바이스 ID가 변경되어 어트리뷰션이 중단될 수 있습니다.
이를 방지하려면 항상 각 광고 네트워크에 적합한 Singular 추적 링크 형식을 사용하세요:
Singular 웹SDK 데모
아래는 문서에 설명된 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';
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 배너
글로벌 속성
Singular SDK를 사용하면 앱에서 전송되는 모든 세션 및 이벤트와 함께 Singular 서버로 전송할 사용자 지정 속성을 정의할 수 있습니다. 이러한 프로퍼티는 사용자, 앱 모드/상태 등 원하는 모든 정보를 나타낼 수 있습니다.
-
유효한 JSON 객체로 최대 5개의 글로벌 프로퍼티를 정의할 수 있습니다. 전역 속성은 지워지거나 브라우저 컨텍스트가 변경될 때까지 브라우저
localstorage에 유지됩니다. -
각 속성 이름과 값은 최대 200자까지 입력할 수 있습니다. 더 긴 속성 이름이나 값을 전달하면 200자로 잘립니다.
-
글로벌 속성은 현재 Singular의 사용자 수준 이벤트 로그(어트리뷰션 로그 내보내기 참조)와 포스트백에 반영됩니다.
-
글로벌 프로퍼티는 매칭이 필요한 경우 Singular에서 제3자에게 포스트백으로 전송할 때 사용할 수 있습니다.
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);
}
/** SDK 초기화 전에 Singular 글로벌 프로퍼티를 설정합니다. * 최대 5개의 키/값 쌍을 허용합니다. 선택적으로 키의 기존 값을 덮어씁니다. * @param {string} propertyKey - 설정할 속성 키입니다. * @param {string} propertyValue - 설정할 속성 값입니다.
* @param {boolean} overideExisting - 프로퍼티가 이미 존재하는 경우 덮어쓸지 여부 */ // 사용법 window.singularSdk.setGlobalProperties(propertyKey, propertyValue, overideExisting);
// Get the JSON Object of global property values.
// Usage
window.singularSdk.getGlobalProperties();
// Clears all global property values.
// Usage
window.singularSdk.clearGlobalProperties();
자연 검색 추적
중요! 이 예시는 자연 검색 추적을 활성화하기 위한 해결 방법으로 제공됩니다. 이 코드는 예시로만 사용하고 마케팅 부서의 필요에 따라 웹 개발자가 업데이트 및 유지 관리해야 합니다. 자연 검색 추적은 광고주마다 다른 의미를 가질 수 있습니다. 샘플을 검토하여 필요에 맞게 조정하시기 바랍니다.
왜 사용하나요?
-
캠페인 매개변수가 없는 경우에도 자연 검색 방문이 제대로 추적되도록 합니다.
-
명확한 어트리뷰션을 위해 Singular '소스' 파라미터
wpsrc에 (리퍼러)의 값과 '캠페인 이름' 파라미터wpcn를 'OrganicSearch'로 URL에 추가합니다. -
나중에 사용할 수 있도록 현재 URL과 리퍼러를
localStorage에 저장합니다. -
순수 자바Script, 제로 종속성, 손쉬운 연동.
작동 방식
-
페이지 URL에서 (구글, 페이스북, 틱톡, 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(고급): Singular 디바이스 ID 수동 설정
Singular SDK가 자동으로 디바이스 ID를 유지하지 않도록 하려면 최상위 도메인 쿠키 또는 서버 측 쿠키를 사용하여 도메인 전체에서 수동으로 ID를 유지할 수 있습니다. 이 값은 이전에 Singular에서 생성한 유효한 uuid4 형식의 ID여야 합니다.
참고: init 메서드를 호출한 후 singularSdk.getSingularDeviceId()를 사용하여 Singular 디바이스 ID를 읽거나 InitFinishedCallback을 사용할 수 있습니다.
| withPersistentSingularDeviceId 메서드 | |
|---|---|
|
설명 |
지속하려는 Singular 디바이스 ID를 포함한 구성 옵션으로 SDK를 초기화합니다. |
| 시그니처 | 위드퍼시스턴트Singular디바이스아이디(singularDeviceId) |
| 사용 예시 |
|
다음 단계
- 웹 캠페인용 웹사이트 링크를 Singular로 생성하기
- 모바일 인벤토리에 대한 웹 캠페인을 실행하는 경우, 구글 광고 웹, 페이스북 웹, 틱톡 광고 웹에 대한 가이드를 따르세요.
- Singular 보고서에서 데이터 모니터링