@adstage/web-sdk 3.0.14 → 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.14",
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 등)
@@ -33,6 +42,14 @@ export interface TrackingParams {
33
42
  creative_id?: string; // 광고소재 ID
34
43
  term?: string; // 키워드
35
44
 
45
+ // ========== 카카오 키워드광고 파라미터 ==========
46
+ k_campaign?: string; // 카카오 키워드광고 캠페인 ID
47
+ k_adgroup?: string; // 카카오 키워드광고 광고그룹 ID
48
+ k_keyword?: string; // 카카오 키워드광고 등록 키워드
49
+ k_keyword_id?: string; // 카카오 키워드광고 키워드 ID
50
+ k_creative?: string; // 카카오 키워드광고 소재 ID
51
+ k_rank?: string; // 카카오 키워드광고 노출 순위
52
+
36
53
  // ========== UTM 파라미터 ==========
37
54
  utm_source?: string; // 트래픽 소스 (예: google, facebook, newsletter)
38
55
  utm_medium?: string; // 마케팅 매체 (예: cpc, banner, email)
@@ -48,10 +65,13 @@ export interface TrackingParams {
48
65
  export class TrackingParamsModule {
49
66
  private static readonly STORAGE_KEY = 'adstage_tracking_params';
50
67
 
51
- // 추출할 파라미터 키 정의
52
- 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
+ ];
53
73
 
54
- private static readonly CAMPAIGN_KEYS = ['channel', 'campaign', 'campaign_id', 'ad_group', 'ad_group_id', 'ad_creative', 'creative_id', 'term'];
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'];
55
75
 
56
76
  private static readonly UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
57
77
 
@@ -123,13 +143,21 @@ export class TrackingParamsModule {
123
143
  if (hasParams) {
124
144
  // 클릭 ID 기반으로 channel 자동 추론 (channel이 없는 경우)
125
145
  if (!params.channel) {
126
- if (params.gclid) params.channel = 'google.ads';
146
+ // 백엔드 detectChannel 동일한 우선순위/채널명 유지
147
+ if (params.gclid || params.gbraid || params.wbraid) params.channel = 'google.ads';
127
148
  else if (params.fbclid) params.channel = 'meta.ads';
128
149
  else if (params.ttclid) params.channel = 'tiktok.ads';
129
- else if (params.nclid) params.channel = 'naver.searchad';
130
- else if (params.liclid) params.channel = 'linkedin.ads';
131
- else if (params.msclkid) params.channel = 'microsoft.ads';
132
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';
158
+ // 카카오 키워드광고: utm_source=kakao & utm_medium=keyword 또는 k_ 파라미터
159
+ else if (params.utm_source === 'kakao' && params.utm_medium === 'keyword') params.channel = 'kakao.keyword';
160
+ else if (params.k_campaign || params.k_keyword) params.channel = 'kakao.keyword';
133
161
  }
134
162
 
135
163
  TrackingParamsModule.store(params);
@@ -315,6 +343,14 @@ export class TrackingParamsModule {
315
343
  if (params.creative_id) attribution.creativeId = params.creative_id;
316
344
  if (params.term) attribution.term = params.term;
317
345
 
346
+ // ========== 카카오 키워드광고 파라미터 ==========
347
+ if (params.k_campaign) attribution.campaignId = attribution.campaignId || params.k_campaign;
348
+ if (params.k_adgroup) attribution.adGroupId = attribution.adGroupId || params.k_adgroup;
349
+ if (params.k_keyword) attribution.keyword = params.k_keyword;
350
+ if (params.k_keyword_id) attribution.keywordId = params.k_keyword_id;
351
+ if (params.k_creative) attribution.creativeId = attribution.creativeId || params.k_creative;
352
+ if (params.k_rank) attribution.adRank = params.k_rank;
353
+
318
354
  // ========== UTM 파라미터 (앱/웹 공통 - snake_case 유지) ==========
319
355
  if (params.utm_source) attribution.utm_source = params.utm_source;
320
356
  if (params.utm_medium) attribution.utm_medium = params.utm_medium;
@@ -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
 
@@ -6,11 +6,14 @@
6
6
  export class ApiHeaders {
7
7
  /**
8
8
  * 표준 API 헤더 생성
9
+ *
10
+ * 주의: `User-Agent`는 브라우저 forbidden header라 fetch에서 수동 설정하면 안 된다.
11
+ * (Chromium은 무시하지만 Safari/WebKit은 설정값을 CORS preflight에 포함시켜
12
+ * 서버가 허용하지 않으면 요청 자체가 차단됨.) 브라우저가 모든 요청에 진짜
13
+ * `User-Agent`를 자동 첨부하므로, 서버는 `req.headers['user-agent']`로 동일한 값을 받는다.
9
14
  */
10
15
  static create(apiKey: string, options?: {
11
16
  contentType?: string;
12
- userAgent?: string;
13
- currentUrl?: string;
14
17
  }): Record<string, string> {
15
18
  if (!apiKey) {
16
19
  throw new Error('API key is required');
@@ -21,32 +24,20 @@ export class ApiHeaders {
21
24
  'Content-Type': options?.contentType || 'application/json'
22
25
  };
23
26
 
24
- // User-Agent는 이벤트 추적에서 실제로 사용됨
25
- if (typeof navigator !== 'undefined') {
26
- headers['User-Agent'] = options?.userAgent || navigator.userAgent;
27
- }
28
-
29
- // X-Current-URL은 현재 서버에서 사용하지 않으므로 제거
30
- // 필요시 이벤트 데이터 body에 포함
27
+ // X-Current-URL 등 부가 정보는 HTTP 헤더가 아닌 이벤트 데이터 body에 포함
31
28
 
32
29
  return headers;
33
30
  }
34
31
 
35
32
  /**
36
33
  * 이벤트 추적용 헤더 생성
37
- * User-Agent는 서버에서 실제로 사용됨
34
+ *
35
+ * 디바이스 `userAgent`는 HTTP 헤더가 아니라 요청 body(eventData) 또는 브라우저가
36
+ * 자동 첨부하는 `User-Agent`로 전달된다. (서버 advertisement 컨트롤러는
37
+ * `req.headers['user-agent']`를 직접 사용.) 헤더 수동 override는 Safari CORS
38
+ * preflight를 깨뜨리므로 하지 않는다. 시그니처는 호출처 호환을 위해 유지.
38
39
  */
39
- static createForEvents(apiKey: string, eventData?: Record<string, any>): Record<string, string> {
40
- const baseHeaders = ApiHeaders.create(apiKey);
41
-
42
- // User-Agent 오버라이드 (서버에서 실제 사용)
43
- if (eventData?.userAgent) {
44
- baseHeaders['User-Agent'] = eventData.userAgent;
45
- }
46
-
47
- // 다른 정보들은 HTTP 헤더가 아닌 이벤트 데이터 body에 포함하는 것이 적절
48
- // (currentUrl, referrer 등은 POST body로 전송)
49
-
50
- return baseHeaders;
40
+ static createForEvents(apiKey: string, _eventData?: Record<string, any>): Record<string, string> {
41
+ return ApiHeaders.create(apiKey);
51
42
  }
52
43
  }
@@ -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
+ }