@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adstage/web-sdk",
3
- "version": "3.0.15",
3
+ "version": "3.0.16",
4
4
  "description": "AdStage Web SDK - Production-ready marketing platform SDK with React Provider support for seamless integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
@@ -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
  */
@@ -3,7 +3,7 @@
3
3
  * Firebase Analytics와 유사한 간단한 API 제공
4
4
  */
5
5
 
6
- import AdStage from '../core/adstage';
6
+ import AdStage from '../core/AdStage';
7
7
  import type { EventProperties } from '../modules/events/events-module';
8
8
 
9
9
  /**
package/src/index.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  // 메인 네임스페이스 클래스
7
- export { default as AdStage } from './core/adstage';
8
- import AdStageCore from './core/adstage';
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
- msclkid?: string; // Microsoft Ads Click ID
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 = ['gclid', 'fbclid', 'ttclid', 'nclid', 'liclid', 'msclkid', 'twclid'];
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
- if (params.gclid) params.channel = 'google.ads';
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/adstage';
7
+ import AdStage from '../core/AdStage';
8
8
  import { AdStageConfig } from '../types/config';
9
9
 
10
10
  interface AdStageContextType {
@@ -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/adstage');
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
+ }