@adstage/web-sdk 3.0.9 → 3.0.11

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.
@@ -0,0 +1,318 @@
1
+ /**
2
+ * AdStage SDK - Tracking Parameters Module
3
+ * URL에서 광고 추적 파라미터를 자동으로 추출하여 저장
4
+ *
5
+ * 지원 파라미터:
6
+ * - 클릭 ID: gclid, fbclid, ttclid, nclid (플랫폼별)
7
+ * - 캠페인 정보: campaign, campaign_id, ad_group, ad_creative, term
8
+ * - UTM 파라미터: utm_source, utm_medium, utm_campaign, utm_content, utm_term
9
+ *
10
+ * 동작 방식:
11
+ * 1. SDK 초기화 시 URL에서 파라미터 자동 추출
12
+ * 2. sessionStorage에 저장 (SPA 페이지 전환 시에도 유지)
13
+ * 3. EventsModule에서 track() 호출 시 자동으로 attribution 정보 주입
14
+ */
15
+
16
+ export interface TrackingParams {
17
+ // ========== 클릭 ID (광고 플랫폼별) ==========
18
+ gclid?: string; // Google Ads Click ID
19
+ fbclid?: string; // Meta Ads (Facebook) Click ID
20
+ ttclid?: string; // TikTok Ads Click ID
21
+ nclid?: string; // Naver SA Click ID
22
+ liclid?: string; // LinkedIn Ads Click ID
23
+ msclkid?: string; // Microsoft Ads Click ID
24
+ twclid?: string; // Twitter Ads Click ID
25
+
26
+ // ========== 광고 계층 정보 ==========
27
+ channel?: string; // 광고 채널 (google.ads, meta.ads, naver.searchad 등)
28
+ campaign?: string; // 캠페인명/ID
29
+ campaign_id?: string; // 캠페인 ID
30
+ ad_group?: string; // 광고그룹명/ID
31
+ ad_group_id?: string; // 광고그룹 ID
32
+ ad_creative?: string; // 광고소재명/ID
33
+ creative_id?: string; // 광고소재 ID
34
+ term?: string; // 키워드
35
+
36
+ // ========== UTM 파라미터 ==========
37
+ utm_source?: string; // 트래픽 소스 (예: google, facebook, newsletter)
38
+ utm_medium?: string; // 마케팅 매체 (예: cpc, banner, email)
39
+ utm_campaign?: string; // 캠페인 이름
40
+ utm_content?: string; // 광고 콘텐츠 구분자
41
+ utm_term?: string; // 검색 키워드
42
+ }
43
+
44
+ /**
45
+ * TrackingParams 모듈
46
+ * URL 파라미터 추출 및 저장 관리
47
+ */
48
+ export class TrackingParamsModule {
49
+ private static readonly STORAGE_KEY = 'adstage_tracking_params';
50
+
51
+ // 추출할 파라미터 키 정의
52
+ private static readonly CLICK_ID_KEYS = ['gclid', 'fbclid', 'ttclid', 'nclid', 'liclid', 'msclkid', 'twclid'];
53
+
54
+ private static readonly CAMPAIGN_KEYS = ['campaign', 'campaign_id', 'ad_group', 'ad_group_id', 'ad_creative', 'creative_id', 'term'];
55
+
56
+ private static readonly UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
57
+
58
+ private static readonly ALL_KEYS = [
59
+ ...TrackingParamsModule.CLICK_ID_KEYS,
60
+ ...TrackingParamsModule.CAMPAIGN_KEYS,
61
+ ...TrackingParamsModule.UTM_KEYS,
62
+ ];
63
+
64
+ /**
65
+ * 현재 URL에서 추적 파라미터 추출 및 저장
66
+ * SDK 초기화 시 자동 호출됨
67
+ *
68
+ * 지원 형식:
69
+ * 1. 일반 URL 파라미터: ?gclid=abc123&campaign=summer
70
+ * 2. adstage_referrer 인코딩 파라미터: ?adstage_referrer=channel%3Dgoogle.ads%26gclid%3Dabc123
71
+ */
72
+ public static captureFromUrl(): TrackingParams | null {
73
+ if (typeof window === 'undefined') {
74
+ return null; // SSR 환경에서는 실행 안 함
75
+ }
76
+
77
+ const urlParams = new URLSearchParams(window.location.search);
78
+ const params: TrackingParams = {};
79
+ let hasParams = false;
80
+
81
+ // 1️⃣ adstage_referrer 파라미터 우선 처리 (최종 URL 접미사 방식)
82
+ const adstageReferrer = urlParams.get('adstage_referrer');
83
+ if (adstageReferrer) {
84
+ try {
85
+ // URL 디코딩: channel%3Dgoogle.ads%26gclid%3Dabc123 → channel=google.ads&gclid=abc123
86
+ const decoded = decodeURIComponent(adstageReferrer);
87
+ const referrerParams = new URLSearchParams(decoded);
88
+
89
+ // 추적 파라미터 추출
90
+ for (const key of TrackingParamsModule.ALL_KEYS) {
91
+ const value = referrerParams.get(key);
92
+ if (value) {
93
+ params[key as keyof TrackingParams] = value;
94
+ hasParams = true;
95
+ }
96
+ }
97
+
98
+ // adstage=true 플래그 확인
99
+ const isAdstage = referrerParams.get('adstage');
100
+ if (isAdstage === 'true') {
101
+ hasParams = true; // Adstage 추적 확인
102
+ }
103
+ } catch (error) {
104
+ console.warn('Failed to parse adstage_referrer:', error);
105
+ }
106
+ }
107
+
108
+ // 2️⃣ 일반 URL 파라미터 추출 (추적 템플릿 방식 또는 직접 입력)
109
+ for (const key of TrackingParamsModule.ALL_KEYS) {
110
+ // adstage_referrer에서 이미 추출했으면 건너뛰기 (중복 방지)
111
+ if (params[key as keyof TrackingParams]) {
112
+ continue;
113
+ }
114
+
115
+ const value = urlParams.get(key);
116
+ if (value) {
117
+ params[key as keyof TrackingParams] = value;
118
+ hasParams = true;
119
+ }
120
+ }
121
+
122
+ // 파라미터가 있으면 저장
123
+ if (hasParams) {
124
+ // 클릭 ID 기반으로 channel 자동 추론 (channel이 없는 경우)
125
+ if (!params.channel) {
126
+ if (params.gclid) params.channel = 'google.ads';
127
+ else if (params.fbclid) params.channel = 'meta.ads';
128
+ 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
+ else if (params.twclid) params.channel = 'twitter.ads';
133
+ }
134
+
135
+ TrackingParamsModule.store(params);
136
+ return params;
137
+ }
138
+
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * 추적 파라미터를 sessionStorage + localStorage에 저장
144
+ * (SPA 페이지 전환 + 브라우저 재시작 시에도 유지)
145
+ */
146
+ public static store(params: TrackingParams): void {
147
+ if (typeof window === 'undefined') {
148
+ return;
149
+ }
150
+
151
+ try {
152
+ // 기존 파라미터와 병합 (새 파라미터 우선)
153
+ const existing = TrackingParamsModule.get();
154
+ const merged = { ...existing, ...params };
155
+ const json = JSON.stringify(merged);
156
+
157
+ try { sessionStorage.setItem(TrackingParamsModule.STORAGE_KEY, json); } catch (_) {}
158
+ try { localStorage.setItem(TrackingParamsModule.STORAGE_KEY, json); } catch (_) {}
159
+ } catch (error) {
160
+ console.warn('Failed to store tracking params:', error);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * 저장된 추적 파라미터 반환
166
+ * 우선순위: sessionStorage (현재 세션) > localStorage (영속 저장)
167
+ * EventsModule에서 track() 호출 시 사용됨
168
+ */
169
+ public static get(): TrackingParams | null {
170
+ if (typeof window === 'undefined') {
171
+ return null;
172
+ }
173
+
174
+ try {
175
+ const fromSession = sessionStorage?.getItem(TrackingParamsModule.STORAGE_KEY);
176
+ if (fromSession) return JSON.parse(fromSession) as TrackingParams;
177
+ } catch (_) {}
178
+
179
+ try {
180
+ const fromLocal = localStorage?.getItem(TrackingParamsModule.STORAGE_KEY);
181
+ if (fromLocal) return JSON.parse(fromLocal) as TrackingParams;
182
+ } catch (_) {}
183
+
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * 저장된 추적 파라미터 삭제
189
+ */
190
+ public static clear(): void {
191
+ if (typeof window === 'undefined') {
192
+ return;
193
+ }
194
+
195
+ try { sessionStorage?.removeItem(TrackingParamsModule.STORAGE_KEY); } catch (_) {}
196
+ try { localStorage?.removeItem(TrackingParamsModule.STORAGE_KEY); } catch (_) {}
197
+ }
198
+
199
+ /**
200
+ * 현재 추적 파라미터 보유 여부 확인
201
+ */
202
+ public static hasParams(): boolean {
203
+ const params = TrackingParamsModule.get();
204
+ return params !== null && Object.keys(params).length > 0;
205
+ }
206
+
207
+ /**
208
+ * 특정 클릭 ID 보유 여부 확인
209
+ * 어트리뷰션이 가능한 상태인지 체크
210
+ */
211
+ public static hasClickId(): boolean {
212
+ const params = TrackingParamsModule.get();
213
+ if (!params) return false;
214
+
215
+ return TrackingParamsModule.CLICK_ID_KEYS.some((key) => params[key as keyof TrackingParams]);
216
+ }
217
+
218
+ /**
219
+ * Click 이벤트 자동 전송 여부 확인
220
+ * 새로운 클릭 ID가 발견되면 true 반환
221
+ */
222
+ public static shouldSendClickEvent(): boolean {
223
+ if (typeof window === 'undefined') {
224
+ return false;
225
+ }
226
+
227
+ const params = TrackingParamsModule.get();
228
+ if (!params || !TrackingParamsModule.hasClickId()) {
229
+ return false;
230
+ }
231
+
232
+ // 현재 클릭 ID 추출
233
+ const currentClickId =
234
+ params.gclid || params.fbclid || params.ttclid || params.nclid || params.liclid || params.msclkid || params.twclid;
235
+
236
+ if (!currentClickId) {
237
+ return false;
238
+ }
239
+
240
+ // 이미 처리된 클릭 ID인지 확인 (중복 방지)
241
+ const lastClickId = sessionStorage.getItem('adstage_last_click_id');
242
+
243
+ // 새로운 클릭 ID면 전송 필요
244
+ if (currentClickId !== lastClickId) {
245
+ sessionStorage.setItem('adstage_last_click_id', currentClickId);
246
+ return true;
247
+ }
248
+
249
+ return false;
250
+ }
251
+
252
+ /**
253
+ * Click 이벤트 전송 완료 마킹
254
+ */
255
+ public static markClickEventSent(): void {
256
+ if (typeof window === 'undefined') {
257
+ return;
258
+ }
259
+
260
+ sessionStorage.setItem('adstage_click_event_sent', 'true');
261
+ }
262
+
263
+ /**
264
+ * Click 이벤트 전송 여부 확인
265
+ */
266
+ public static isClickEventSent(): boolean {
267
+ if (typeof window === 'undefined') {
268
+ return false;
269
+ }
270
+
271
+ return sessionStorage.getItem('adstage_click_event_sent') === 'true';
272
+ }
273
+
274
+ /**
275
+ * Event 스키마의 attribution 객체로 변환
276
+ * EventsModule에서 사용
277
+ *
278
+ * 변환 규칙:
279
+ * - 클릭 ID, channel: 그대로 전달
280
+ * - 광고 플랫폼 파라미터: snake_case → camelCase 변환 (웹 호환)
281
+ * - UTM 파라미터: snake_case 그대로 유지 (앱/웹 공통 표준)
282
+ */
283
+ public static toAttributionObject(): Record<string, any> | null {
284
+ const params = TrackingParamsModule.get();
285
+ if (!params) return null;
286
+
287
+ // API Event 스키마의 attribution 필드 형식으로 변환
288
+ const attribution: Record<string, any> = {};
289
+
290
+ // ========== 클릭 ID (플랫폼별 필드) ==========
291
+ if (params.gclid) attribution.gclid = params.gclid;
292
+ if (params.fbclid) attribution.fbclid = params.fbclid;
293
+ if (params.ttclid) attribution.ttclid = params.ttclid;
294
+ if (params.nclid) attribution.nclid = params.nclid;
295
+
296
+ // ========== 채널 정보 ==========
297
+ if (params.channel) attribution.channel = params.channel;
298
+
299
+ // ========== 광고 플랫폼 파라미터 (웹 변환용 - camelCase) ==========
300
+ // 웹 SDK에서는 utm_campaign을 campaign으로 매핑할 수 있음
301
+ if (params.campaign) attribution.campaign = params.campaign;
302
+ if (params.campaign_id) attribution.campaignId = params.campaign_id;
303
+ if (params.ad_group) attribution.adGroup = params.ad_group;
304
+ if (params.ad_group_id) attribution.adGroupId = params.ad_group_id;
305
+ if (params.ad_creative) attribution.adCreative = params.ad_creative;
306
+ if (params.creative_id) attribution.creativeId = params.creative_id;
307
+ if (params.term) attribution.term = params.term;
308
+
309
+ // ========== UTM 파라미터 (앱/웹 공통 - snake_case 유지) ==========
310
+ if (params.utm_source) attribution.utm_source = params.utm_source;
311
+ if (params.utm_medium) attribution.utm_medium = params.utm_medium;
312
+ if (params.utm_campaign) attribution.utm_campaign = params.utm_campaign;
313
+ if (params.utm_content) attribution.utm_content = params.utm_content;
314
+ if (params.utm_term) attribution.utm_term = params.utm_term;
315
+
316
+ return Object.keys(attribution).length > 0 ? attribution : null;
317
+ }
318
+ }
@@ -1,7 +1,5 @@
1
1
  // 이벤트 및 추적 관련 타입
2
2
  export interface SessionInfo {
3
- sessionId: string;
4
- userId?: string;
5
3
  startTime: number;
6
4
  lastActivity: number;
7
5
  pageViews: number;
@@ -1,183 +0,0 @@
1
- /**
2
- * AdStage SDK - 이벤트용 세션 관리자
3
- * 세션 ID 생성 및 사용자 ID 관리
4
- */
5
-
6
- import { DOMUtils } from '../../utils/dom-utils';
7
-
8
- export class EventSessionManager {
9
- private static _userId?: string;
10
- private static _sessionStartTime?: number;
11
-
12
- /**
13
- * 세션 ID 생성 및 반환 (SSR 안전)
14
- */
15
- static getSessionId(): string {
16
- if (!DOMUtils.isBrowser()) {
17
- return 'ssr_session_' + Date.now();
18
- }
19
-
20
- const stored = sessionStorage.getItem('adstage_session_id');
21
- if (stored) {
22
- return stored;
23
- }
24
-
25
- const sessionId = 'session_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
26
- sessionStorage.setItem('adstage_session_id', sessionId);
27
-
28
- // 세션 시작 시간 기록
29
- if (!EventSessionManager._sessionStartTime) {
30
- EventSessionManager._sessionStartTime = Date.now();
31
- if (DOMUtils.isBrowser()) {
32
- sessionStorage.setItem('adstage_session_start', String(EventSessionManager._sessionStartTime));
33
- }
34
- }
35
-
36
- return sessionId;
37
- }
38
-
39
- /**
40
- * 사용자 ID 설정
41
- */
42
- static setUserId(userId: string): void {
43
- EventSessionManager._userId = userId;
44
-
45
- if (DOMUtils.isBrowser()) {
46
- localStorage.setItem('adstage_user_id', userId);
47
- }
48
- }
49
-
50
- /**
51
- * 현재 사용자 ID 반환
52
- */
53
- static getUserId(): string | undefined {
54
- // 메모리에 있는 값 우선 반환
55
- if (EventSessionManager._userId) {
56
- return EventSessionManager._userId;
57
- }
58
-
59
- // 로컬 스토리지에서 복원
60
- if (DOMUtils.isBrowser()) {
61
- const stored = localStorage.getItem('adstage_user_id');
62
- if (stored) {
63
- EventSessionManager._userId = stored;
64
- return stored;
65
- }
66
- }
67
-
68
- return undefined;
69
- }
70
-
71
- /**
72
- * 사용자 ID 제거
73
- */
74
- static clearUserId(): void {
75
- EventSessionManager._userId = undefined;
76
-
77
- if (DOMUtils.isBrowser()) {
78
- localStorage.removeItem('adstage_user_id');
79
- }
80
- }
81
-
82
- /**
83
- * 세션 정보 전체 반환
84
- */
85
- static getSessionInfo(): {
86
- sessionId: string;
87
- userId?: string;
88
- sessionDuration?: number;
89
- isNewSession: boolean;
90
- } {
91
- const sessionId = EventSessionManager.getSessionId();
92
- const userId = EventSessionManager.getUserId();
93
- const isNewSession = EventSessionManager.isNewSession();
94
- const sessionDuration = EventSessionManager.getSessionDuration();
95
-
96
- return {
97
- sessionId,
98
- userId,
99
- sessionDuration,
100
- isNewSession
101
- };
102
- }
103
-
104
- /**
105
- * 새 세션인지 확인
106
- */
107
- static isNewSession(): boolean {
108
- if (!DOMUtils.isBrowser()) return true;
109
-
110
- const sessionId = sessionStorage.getItem('adstage_session_id');
111
- const sessionStart = sessionStorage.getItem('adstage_session_start');
112
-
113
- return !sessionId || !sessionStart;
114
- }
115
-
116
- /**
117
- * 세션 지속 시간 반환 (ms)
118
- */
119
- static getSessionDuration(): number {
120
- if (!DOMUtils.isBrowser()) return 0;
121
-
122
- const sessionStart = sessionStorage.getItem('adstage_session_start');
123
- if (!sessionStart) return 0;
124
-
125
- return Date.now() - parseInt(sessionStart, 10);
126
- }
127
-
128
- /**
129
- * 세션 새로고침 (새로운 세션 ID 생성)
130
- */
131
- static refreshSession(): string {
132
- if (DOMUtils.isBrowser()) {
133
- sessionStorage.removeItem('adstage_session_id');
134
- sessionStorage.removeItem('adstage_session_start');
135
- }
136
-
137
- EventSessionManager._sessionStartTime = undefined;
138
- return EventSessionManager.getSessionId();
139
- }
140
-
141
- /**
142
- * 세션 만료 확인 (24시간 기준)
143
- */
144
- static isSessionExpired(maxDurationHours: number = 24): boolean {
145
- const duration = EventSessionManager.getSessionDuration();
146
- const maxDuration = maxDurationHours * 60 * 60 * 1000; // ms 변환
147
-
148
- return duration > maxDuration;
149
- }
150
-
151
- /**
152
- * 세션 통계 반환 (디버깅용)
153
- */
154
- static getSessionStats(): {
155
- sessionId: string;
156
- userId?: string;
157
- startTime?: number;
158
- duration: number;
159
- isExpired: boolean;
160
- isNewSession: boolean;
161
- } {
162
- const sessionId = EventSessionManager.getSessionId();
163
- const userId = EventSessionManager.getUserId();
164
- const duration = EventSessionManager.getSessionDuration();
165
- const isExpired = EventSessionManager.isSessionExpired();
166
- const isNewSession = EventSessionManager.isNewSession();
167
-
168
- let startTime: number | undefined;
169
- if (DOMUtils.isBrowser()) {
170
- const stored = sessionStorage.getItem('adstage_session_start');
171
- startTime = stored ? parseInt(stored, 10) : undefined;
172
- }
173
-
174
- return {
175
- sessionId,
176
- userId,
177
- startTime,
178
- duration,
179
- isExpired,
180
- isNewSession
181
- };
182
- }
183
- }