@adstage/web-sdk 3.0.15 → 3.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.esm.js +1 -1
- package/dist/index.standalone.js +31 -2
- package/dist/index.umd.js +31 -2
- package/package.json +1 -1
- package/src/core/{adstage.ts → AdStage.ts} +31 -0
- package/src/events/global-events.ts +1 -1
- package/src/index.ts +2 -2
- package/src/modules/tracking/tracking-params.ts +24 -7
- package/src/react/ad-stage-provider.tsx +1 -1
- package/src/types/config.ts +7 -1
- package/src/utils/config-utils.ts +1 -1
- package/src/utils/tracking-link-decorator.ts +135 -0
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import { EventsModule } from '../modules/events/events-module';
|
|
|
10
10
|
import { ViewableEventTracker } from '../managers/ads/viewable-event-tracker';
|
|
11
11
|
import { endpoints } from '../constants/endpoints';
|
|
12
12
|
import { TrackingParamsModule } from '../modules/tracking/tracking-params';
|
|
13
|
+
import { installTrackingLinkDecorator, decorateTrackingUrl } from '../utils/tracking-link-decorator';
|
|
13
14
|
|
|
14
15
|
export class AdStage {
|
|
15
16
|
private static instance: AdStage;
|
|
@@ -100,6 +101,17 @@ export class AdStage {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
// 🔗 아웃바운드 트래킹 링크(adg.im) 데코레이터 설치.
|
|
105
|
+
// 페이지의 adg.im 링크 클릭 시 방문자 신원(클릭 ID + attribution_id)을 부착 → redirect 서버가
|
|
106
|
+
// Play Install Referrer 로 전달 → 네이티브 설치가 원본 채널(google.ads 등) 인계 (web→app 연결).
|
|
107
|
+
// 신원 값은 클릭 시점에 읽으므로(비동기 attribution 등록 이후 반영) getter 로 전달한다.
|
|
108
|
+
installTrackingLinkDecorator({
|
|
109
|
+
getClickParams: () => TrackingParamsModule.get(),
|
|
110
|
+
getAttributionId: () => instance.events.getAttributionId(),
|
|
111
|
+
domains: instance._config?.trackingLinkDomains,
|
|
112
|
+
debug: config.debug,
|
|
113
|
+
});
|
|
114
|
+
|
|
103
115
|
// 🎯 Click 이벤트 자동 전송 (새로운 클릭 ID 발견 시)
|
|
104
116
|
const shouldSend = TrackingParamsModule.shouldSendClickEvent();
|
|
105
117
|
if (config.debug) {
|
|
@@ -237,6 +249,25 @@ export class AdStage {
|
|
|
237
249
|
return AdStage.instance?._config || null;
|
|
238
250
|
}
|
|
239
251
|
|
|
252
|
+
/**
|
|
253
|
+
* 트래킹 링크(adg.im 등)에 현재 방문자 신원(클릭 ID + attribution_id)을 부착해 반환.
|
|
254
|
+
* <a> 클릭은 자동 데코레이션되지만, window.open() 등 프로그램적 이동에는 이 헬퍼를 직접 사용한다.
|
|
255
|
+
* 트래킹 도메인이 아니면 원본 URL 을 그대로 반환한다.
|
|
256
|
+
*/
|
|
257
|
+
public static decorateUrl(url: string): string {
|
|
258
|
+
try {
|
|
259
|
+
const instance = AdStage.getInstance();
|
|
260
|
+
return decorateTrackingUrl(url, {
|
|
261
|
+
clickParams: TrackingParamsModule.get(),
|
|
262
|
+
attributionId: instance.events.getAttributionId(),
|
|
263
|
+
domains: instance._config?.trackingLinkDomains,
|
|
264
|
+
baseHref: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
265
|
+
});
|
|
266
|
+
} catch (_) {
|
|
267
|
+
return url;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
240
271
|
/**
|
|
241
272
|
* SDK 인스턴스 반환 (공개 메소드로 변경)
|
|
242
273
|
*/
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// 메인 네임스페이스 클래스
|
|
7
|
-
export { default as AdStage } from './core/
|
|
8
|
-
import AdStageCore from './core/
|
|
7
|
+
export { default as AdStage } from './core/AdStage';
|
|
8
|
+
import AdStageCore from './core/AdStage';
|
|
9
9
|
|
|
10
10
|
// 전역 이벤트 함수들 (Firebase 스타일)
|
|
11
11
|
export { track, setUserProperties, getUserProperties } from './events/global-events';
|
|
@@ -16,12 +16,21 @@
|
|
|
16
16
|
export interface TrackingParams {
|
|
17
17
|
// ========== 클릭 ID (광고 플랫폼별) ==========
|
|
18
18
|
gclid?: string; // Google Ads Click ID
|
|
19
|
+
gbraid?: string; // Google Ads (iOS/web-to-app)
|
|
20
|
+
wbraid?: string; // Google Ads (web-to-web)
|
|
19
21
|
fbclid?: string; // Meta Ads (Facebook) Click ID
|
|
20
22
|
ttclid?: string; // TikTok Ads Click ID
|
|
21
23
|
nclid?: string; // Naver SA Click ID
|
|
24
|
+
naverclk?: string; // Naver SA Click ID (legacy)
|
|
22
25
|
liclid?: string; // LinkedIn Ads Click ID
|
|
23
|
-
|
|
26
|
+
li_fat_id?: string; // LinkedIn Ads Click ID (공식)
|
|
27
|
+
msclkid?: string; // Microsoft Ads (Bing) Click ID
|
|
24
28
|
twclid?: string; // Twitter Ads Click ID
|
|
29
|
+
sccid?: string; // Snapchat Ads Click ID
|
|
30
|
+
ScCid?: string; // Snapchat Ads Click ID (대문자형)
|
|
31
|
+
yclid?: string; // Yahoo Ads Click ID
|
|
32
|
+
pnclid?: string; // Pinterest Ads Click ID
|
|
33
|
+
kakaoclk?: string; // Kakao Moment Click ID
|
|
25
34
|
|
|
26
35
|
// ========== 광고 계층 정보 ==========
|
|
27
36
|
channel?: string; // 광고 채널 (google.ads, meta.ads, naver.searchad 등)
|
|
@@ -56,8 +65,11 @@ export interface TrackingParams {
|
|
|
56
65
|
export class TrackingParamsModule {
|
|
57
66
|
private static readonly STORAGE_KEY = 'adstage_tracking_params';
|
|
58
67
|
|
|
59
|
-
// 추출할 파라미터 키 정의
|
|
60
|
-
private static readonly CLICK_ID_KEYS = [
|
|
68
|
+
// 추출할 파라미터 키 정의 (백엔드 extractClickId/detectChannel 와 동일하게 유지)
|
|
69
|
+
private static readonly CLICK_ID_KEYS = [
|
|
70
|
+
'gclid', 'gbraid', 'wbraid', 'fbclid', 'ttclid', 'twclid', 'li_fat_id', 'liclid',
|
|
71
|
+
'msclkid', 'sccid', 'ScCid', 'yclid', 'pnclid', 'nclid', 'naverclk', 'kakaoclk',
|
|
72
|
+
];
|
|
61
73
|
|
|
62
74
|
private static readonly CAMPAIGN_KEYS = ['channel', 'campaign', 'campaign_id', 'ad_group', 'ad_group_id', 'ad_creative', 'creative_id', 'term', 'k_campaign', 'k_adgroup', 'k_keyword', 'k_keyword_id', 'k_creative', 'k_rank'];
|
|
63
75
|
|
|
@@ -131,13 +143,18 @@ export class TrackingParamsModule {
|
|
|
131
143
|
if (hasParams) {
|
|
132
144
|
// 클릭 ID 기반으로 channel 자동 추론 (channel이 없는 경우)
|
|
133
145
|
if (!params.channel) {
|
|
134
|
-
|
|
146
|
+
// 백엔드 detectChannel 과 동일한 우선순위/채널명 유지
|
|
147
|
+
if (params.gclid || params.gbraid || params.wbraid) params.channel = 'google.ads';
|
|
135
148
|
else if (params.fbclid) params.channel = 'meta.ads';
|
|
136
149
|
else if (params.ttclid) params.channel = 'tiktok.ads';
|
|
137
|
-
else if (params.nclid) params.channel = 'naver.searchad';
|
|
138
|
-
else if (params.liclid) params.channel = 'linkedin.ads';
|
|
139
|
-
else if (params.msclkid) params.channel = 'microsoft.ads';
|
|
140
150
|
else if (params.twclid) params.channel = 'twitter.ads';
|
|
151
|
+
else if (params.li_fat_id || params.liclid) params.channel = 'linkedin.ads';
|
|
152
|
+
else if (params.msclkid) params.channel = 'microsoft.ads';
|
|
153
|
+
else if (params.sccid || params.ScCid) params.channel = 'snapchat.ads';
|
|
154
|
+
else if (params.yclid) params.channel = 'yahoo.ads';
|
|
155
|
+
else if (params.pnclid) params.channel = 'pinterest.ads';
|
|
156
|
+
else if (params.naverclk || params.nclid) params.channel = 'naver.searchad';
|
|
157
|
+
else if (params.kakaoclk) params.channel = 'kakao.moment';
|
|
141
158
|
// 카카오 키워드광고: utm_source=kakao & utm_medium=keyword 또는 k_ 파라미터
|
|
142
159
|
else if (params.utm_source === 'kakao' && params.utm_medium === 'keyword') params.channel = 'kakao.keyword';
|
|
143
160
|
else if (params.k_campaign || params.k_keyword) params.channel = 'kakao.keyword';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
|
7
|
-
import AdStage from '../core/
|
|
7
|
+
import AdStage from '../core/AdStage';
|
|
8
8
|
import { AdStageConfig } from '../types/config';
|
|
9
9
|
|
|
10
10
|
interface AdStageContextType {
|
package/src/types/config.ts
CHANGED
|
@@ -26,7 +26,13 @@ export interface AdStageConfig {
|
|
|
26
26
|
|
|
27
27
|
/** 플레이스홀더 스타일 모드 (기본값: 'subtle') */
|
|
28
28
|
placeholderMode?: 'invisible' | 'transparent' | 'subtle' | 'minimal' | 'debug' | 'legacy';
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 아웃바운드 트래킹 링크 데코레이션 대상 도메인 (기본값: ['adg.im'])
|
|
32
|
+
* 이 도메인으로 향하는 <a> 클릭 시 방문자 신원(클릭 ID + attribution_id)을 자동 부착해
|
|
33
|
+
* web→app 어트리뷰션이 끊기지 않게 한다.
|
|
34
|
+
*/
|
|
35
|
+
trackingLinkDomains?: string[];
|
|
30
36
|
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -12,7 +12,7 @@ export class ConfigUtils {
|
|
|
12
12
|
static getConfig(): AdStageConfig | null {
|
|
13
13
|
// AdStage 클래스 동적 임포트로 순환 참조 방지
|
|
14
14
|
try {
|
|
15
|
-
const { AdStage } = require('../core/
|
|
15
|
+
const { AdStage } = require('../core/AdStage');
|
|
16
16
|
return AdStage.getConfig();
|
|
17
17
|
} catch {
|
|
18
18
|
return null;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 트래킹 링크 데코레이터 (web → app 신원 전달)
|
|
3
|
+
*
|
|
4
|
+
* 웹 페이지에서 adg.im(트래킹/딥링크) 링크로 나가는 클릭 시점에, 현재 방문자의 신원
|
|
5
|
+
* (클릭 ID = gclid 등 + attribution_id)을 해당 링크 URL에 부착한다.
|
|
6
|
+
*
|
|
7
|
+
* redirect 서버(deeplink-redirect.controller)는 이 값을 Google Play Install Referrer 로
|
|
8
|
+
* 전달하고, 네이티브 설치 이벤트가 그 referrer 를 백엔드로 보내 원본 채널(google.ads 등)을
|
|
9
|
+
* 인계받는다. → 검색광고(웹 랜딩)→앱 설치 경로에서 어트리뷰션이 organic 으로 새는 것을 막는다.
|
|
10
|
+
*
|
|
11
|
+
* Google Play Install Referrer API 는 referrer= 파라미터만 캡처하므로, attribution_id 를
|
|
12
|
+
* redirect 단계에서 referrer 문자열 안에 넣어야 한다(서버 측 처리). 여기서는 redirect 서버가
|
|
13
|
+
* 읽을 수 있도록 쿼리로만 전달하면 된다.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// SDK 의 TrackingParamsModule.CLICK_ID_KEYS 와 동일하게 유지 (백엔드 extractClickId 와도 일치)
|
|
17
|
+
const CLICK_ID_KEYS = [
|
|
18
|
+
'gclid', 'gbraid', 'wbraid', 'fbclid', 'ttclid', 'twclid', 'li_fat_id', 'liclid',
|
|
19
|
+
'msclkid', 'sccid', 'ScCid', 'yclid', 'pnclid', 'nclid', 'naverclk', 'kakaoclk',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// 기본 트래킹 도메인 (딥링크 단축 도메인)
|
|
23
|
+
const DEFAULT_TRACKING_DOMAINS = ['adg.im'];
|
|
24
|
+
|
|
25
|
+
function hostMatches(hostname: string, domains: string[]): boolean {
|
|
26
|
+
const h = (hostname || '').toLowerCase();
|
|
27
|
+
return domains.some((d) => {
|
|
28
|
+
const dd = (d || '').toLowerCase();
|
|
29
|
+
return !!dd && (h === dd || h.endsWith('.' + dd));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DecorateInput {
|
|
34
|
+
/** 현재 방문자의 클릭 파라미터 (TrackingParamsModule.get() 결과) */
|
|
35
|
+
clickParams?: Record<string, any> | null;
|
|
36
|
+
/** 현재 방문자의 attribution_id (events.getAttributionId() 결과) */
|
|
37
|
+
attributionId?: string | null;
|
|
38
|
+
/** 데코레이션 대상 도메인 (기본 ['adg.im']) */
|
|
39
|
+
domains?: string[];
|
|
40
|
+
/** 상대 URL 해석용 base (보통 window.location.href) */
|
|
41
|
+
baseHref?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* URL 이 트래킹 도메인으로 향하면 클릭 ID + attribution_id 를 부착한 새 URL 반환.
|
|
46
|
+
* 트래킹 도메인이 아니거나 파싱 실패 시 원본 URL 그대로 반환.
|
|
47
|
+
* 기존에 같은 파라미터가 이미 있으면 덮어쓰지 않는다.
|
|
48
|
+
*/
|
|
49
|
+
export function decorateTrackingUrl(rawUrl: string, input: DecorateInput = {}): string {
|
|
50
|
+
if (!rawUrl) return rawUrl;
|
|
51
|
+
const domains = input.domains && input.domains.length ? input.domains : DEFAULT_TRACKING_DOMAINS;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(rawUrl, input.baseHref);
|
|
55
|
+
if (!hostMatches(url.hostname, domains)) return rawUrl;
|
|
56
|
+
|
|
57
|
+
const clickParams = input.clickParams || null;
|
|
58
|
+
if (clickParams) {
|
|
59
|
+
for (const key of CLICK_ID_KEYS) {
|
|
60
|
+
const value = clickParams[key];
|
|
61
|
+
if (value && !url.searchParams.has(key)) {
|
|
62
|
+
url.searchParams.set(key, String(value));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (input.attributionId && !url.searchParams.has('attribution_id')) {
|
|
68
|
+
url.searchParams.set('attribution_id', input.attributionId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return url.toString();
|
|
72
|
+
} catch (_) {
|
|
73
|
+
return rawUrl;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface InstallDecoratorOptions {
|
|
78
|
+
getClickParams: () => Record<string, any> | null;
|
|
79
|
+
getAttributionId: () => string | null;
|
|
80
|
+
domains?: string[];
|
|
81
|
+
debug?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let installed = false;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* document 레벨 capture-phase 클릭 리스너를 설치한다.
|
|
88
|
+
* 내비게이션 직전에 adg.im 링크의 href 를 신원 파라미터로 갱신 → SPA 동적 링크/새 탭(auxclick) 포함 대응.
|
|
89
|
+
* 신원 값은 클릭 시점에 읽으므로(비동기 attribution 등록 이후 값 반영) getter 로 전달받는다.
|
|
90
|
+
*/
|
|
91
|
+
export function installTrackingLinkDecorator(options: InstallDecoratorOptions): void {
|
|
92
|
+
if (installed) return;
|
|
93
|
+
if (typeof document === 'undefined') return;
|
|
94
|
+
installed = true;
|
|
95
|
+
|
|
96
|
+
const domains = options.domains && options.domains.length ? options.domains : DEFAULT_TRACKING_DOMAINS;
|
|
97
|
+
|
|
98
|
+
const handler = (event: Event) => {
|
|
99
|
+
try {
|
|
100
|
+
const target = event.target as Element | null;
|
|
101
|
+
if (!target || typeof (target as any).closest !== 'function') return;
|
|
102
|
+
const anchor = target.closest('a[href]') as HTMLAnchorElement | null;
|
|
103
|
+
if (!anchor) return;
|
|
104
|
+
|
|
105
|
+
const href = anchor.getAttribute('href');
|
|
106
|
+
if (!href) return;
|
|
107
|
+
|
|
108
|
+
const decorated = decorateTrackingUrl(href, {
|
|
109
|
+
clickParams: options.getClickParams(),
|
|
110
|
+
attributionId: options.getAttributionId(),
|
|
111
|
+
domains,
|
|
112
|
+
baseHref: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (decorated !== href) {
|
|
116
|
+
anchor.setAttribute('href', decorated);
|
|
117
|
+
if (options.debug) {
|
|
118
|
+
// eslint-disable-next-line no-console
|
|
119
|
+
console.log('🔗 [AdStage] Decorated tracking link:', decorated);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (_) {
|
|
123
|
+
// no-op: 데코레이션 실패가 사용자 클릭/내비게이션을 막아선 안 됨
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// capture phase 로 navigation 직전에 실행
|
|
128
|
+
document.addEventListener('click', handler, true);
|
|
129
|
+
document.addEventListener('auxclick', handler, true);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 테스트용: 설치 플래그 리셋 */
|
|
133
|
+
export function __resetTrackingLinkDecorator(): void {
|
|
134
|
+
installed = false;
|
|
135
|
+
}
|