@adstage/web-sdk 3.0.9 → 3.0.10

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.9",
3
+ "version": "3.0.10",
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",
@@ -9,9 +9,9 @@
9
9
  export const API_ENDPOINTS = {
10
10
  /** 프로덕션 환경 */
11
11
  production: 'https://api.adstage.app',
12
-
12
+
13
13
  /** 베타 환경 (기본값) */
14
- beta: 'https://beta-api.adstage.app'
14
+ beta: 'https://beta-api.adstage.app',
15
15
  } as const;
16
16
 
17
17
  /**
@@ -27,13 +27,14 @@ export const API_PATHS = {
27
27
  advertisements: {
28
28
  list: '/advertisements/list',
29
29
  detail: '/advertisements',
30
- events: '/advertisements/events'
30
+ events: '/advertisements/events',
31
31
  },
32
-
32
+
33
33
  /** 이벤트 관련 */
34
34
  events: {
35
35
  track: '/events/track',
36
- }
36
+ registerAttribution: '/events/register/attribution',
37
+ },
37
38
  } as const;
38
39
 
39
40
  /**
@@ -67,8 +68,7 @@ export class EndpointBuilder {
67
68
  advertisements = {
68
69
  list: () => `${this.baseUrl}${API_PATHS.advertisements.list}`,
69
70
  detail: (adId: string) => `${this.baseUrl}${API_PATHS.advertisements.detail}/${adId}`,
70
- events: (adId: string, eventType: string) =>
71
- `${this.baseUrl}${API_PATHS.advertisements.events}/${adId}/${eventType}`
71
+ events: (adId: string, eventType: string) => `${this.baseUrl}${API_PATHS.advertisements.events}/${adId}/${eventType}`,
72
72
  };
73
73
 
74
74
  /**
@@ -76,6 +76,7 @@ export class EndpointBuilder {
76
76
  */
77
77
  events = {
78
78
  track: () => `${this.baseUrl}${API_PATHS.events.track}`,
79
+ registerAttribution: () => `${this.baseUrl}${API_PATHS.events.registerAttribution}`,
79
80
  };
80
81
 
81
82
  /**
@@ -9,12 +9,13 @@ import { ConfigModule } from '../modules/config/config-module';
9
9
  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
+ import { TrackingParamsModule } from '../modules/tracking/tracking-params';
12
13
 
13
14
  export class AdStage {
14
15
  private static instance: AdStage;
15
16
  private _isInitialized = false;
16
17
  private _config: AdStageConfig | null = null;
17
-
18
+
18
19
  // 모듈 인스턴스들
19
20
  public readonly ads: AdsModule;
20
21
  public readonly config: ConfigModule;
@@ -36,7 +37,7 @@ export class AdStage {
36
37
  }
37
38
 
38
39
  const instance = AdStage.instance;
39
-
40
+
40
41
  // 설정 검증
41
42
  if (!config.apiKey) {
42
43
  throw new Error('API key is required for AdStage initialization');
@@ -47,7 +48,7 @@ export class AdStage {
47
48
  timeout: 30000,
48
49
  debug: false,
49
50
  modules: ['ads', 'events', 'config'],
50
- ...config
51
+ ...config,
51
52
  };
52
53
 
53
54
  // 🔧 baseUrl이 설정된 경우 전역 endpoints 객체 업데이트
@@ -60,7 +61,7 @@ export class AdStage {
60
61
 
61
62
  // 모듈 동기 초기화
62
63
  const enabledModules = instance._config.modules || ['ads', 'events', 'config'];
63
-
64
+
64
65
  for (const moduleName of enabledModules) {
65
66
  const module = instance[moduleName as keyof AdStage] as any;
66
67
  if (module && typeof module.init === 'function') {
@@ -70,12 +71,86 @@ export class AdStage {
70
71
 
71
72
  instance._isInitialized = true;
72
73
 
74
+ // 🎯 URL에서 추적 파라미터 자동 추출 (브라우저 환경에만)
75
+ if (typeof window !== 'undefined') {
76
+ const trackingParams = TrackingParamsModule.captureFromUrl();
77
+ if (trackingParams && config.debug) {
78
+ console.log('📊 Tracking parameters captured:', trackingParams);
79
+ }
80
+
81
+ // � 서버에 Attribution 자동 등록 (클릭 ID가 있는 경우)
82
+ if (trackingParams && TrackingParamsModule.hasClickId()) {
83
+ setTimeout(async () => {
84
+ try {
85
+ // Device ID 생성 (브라우저 fingerprint 또는 랜덤)
86
+ const deviceId = instance.generateDeviceId();
87
+
88
+ const attribution = await instance.events.registerAttribution(deviceId);
89
+
90
+ if (attribution && config.debug) {
91
+ console.log('✅ Attribution registered:', {
92
+ attributionId: attribution.attributionId,
93
+ channel: attribution.channel,
94
+ campaign: attribution.campaign,
95
+ expiresAt: attribution.expiresAt,
96
+ });
97
+ }
98
+ } catch (err) {
99
+ console.error('❌ Failed to register attribution:', err);
100
+ }
101
+ }, 100);
102
+ }
103
+
104
+ // �🎯 Click 이벤트 자동 전송 (새로운 클릭 ID 발견 시)
105
+ const shouldSend = TrackingParamsModule.shouldSendClickEvent();
106
+ if (config.debug) {
107
+ console.log('🔍 Should send click event:', shouldSend);
108
+ if (!shouldSend) {
109
+ console.log('⏭️ Skipping click event (duplicate or no click ID)');
110
+ }
111
+ }
112
+
113
+ if (shouldSend) {
114
+ // EventsModule 초기화 완료 대기
115
+ setTimeout(() => {
116
+ try {
117
+ const trackingParams = TrackingParamsModule.get();
118
+ const attribution = TrackingParamsModule.toAttributionObject();
119
+
120
+ // Click 이벤트 전송 (추적 파라미터는 EventsModule에서 자동 주입)
121
+ instance.events
122
+ .track('click', {
123
+ _autoGenerated: true,
124
+ _source: 'tracking_params',
125
+ _timestamp: new Date().toISOString(),
126
+ _platform: 'web',
127
+ })
128
+ .then(() => {
129
+ TrackingParamsModule.markClickEventSent();
130
+
131
+ if (config.debug) {
132
+ console.log('✅ Auto-tracked click event with tracking params:', {
133
+ attribution,
134
+ trackingParams,
135
+ });
136
+ }
137
+ })
138
+ .catch((err) => {
139
+ console.error('❌ Failed to auto-track click event:', err);
140
+ });
141
+ } catch (err) {
142
+ console.error('❌ Error in click event auto-tracking:', err);
143
+ }
144
+ }, 200); // Attribution 등록 후 실행
145
+ }
146
+ }
147
+
73
148
  if (config.debug) {
74
149
  console.log('🚀 AdStage SDK initialized (sync mode)', {
75
150
  version: '2.0.0',
76
151
  modules: enabledModules,
77
152
  apiKey: config.apiKey.substring(0, 8) + '...',
78
- mode: 'development'
153
+ mode: 'development',
79
154
  });
80
155
  }
81
156
  }
@@ -144,6 +219,46 @@ export class AdStage {
144
219
  }
145
220
  }
146
221
 
222
+ /**
223
+ * Device ID 생성 (브라우저 fingerprint 기반)
224
+ * localStorage에 저장하여 재사용
225
+ */
226
+ private generateDeviceId(): string {
227
+ if (typeof window === 'undefined') {
228
+ return 'server-' + Math.random().toString(36).substring(2, 15);
229
+ }
230
+
231
+ // 기존 Device ID 확인
232
+ const existingId = localStorage.getItem('adstage_device_id');
233
+ if (existingId) {
234
+ return existingId;
235
+ }
236
+
237
+ // 새 Device ID 생성 (브라우저 fingerprint 기반)
238
+ const fingerprint = [
239
+ navigator.userAgent,
240
+ navigator.language,
241
+ screen.width + 'x' + screen.height,
242
+ new Date().getTimezoneOffset(),
243
+ navigator.hardwareConcurrency || 0,
244
+ ].join('|');
245
+
246
+ // 간단한 해시 생성
247
+ let hash = 0;
248
+ for (let i = 0; i < fingerprint.length; i++) {
249
+ const char = fingerprint.charCodeAt(i);
250
+ hash = (hash << 5) - hash + char;
251
+ hash = hash & hash;
252
+ }
253
+
254
+ const deviceId = 'web-' + Math.abs(hash).toString(36) + '-' + Date.now().toString(36);
255
+
256
+ // localStorage에 저장
257
+ localStorage.setItem('adstage_device_id', deviceId);
258
+
259
+ return deviceId;
260
+ }
261
+
147
262
  /**
148
263
  * 디버그용 메서드들
149
264
  */
@@ -170,9 +285,9 @@ export class AdStage {
170
285
  getViewableStatus: (): void => {
171
286
  console.log('📊 AdStage Debug: 현재 viewable 추적 상태', {
172
287
  trackedCount: (ViewableEventTracker as any).viewableTracker.size,
173
- trackedItems: Array.from((ViewableEventTracker as any).viewableTracker)
288
+ trackedItems: Array.from((ViewableEventTracker as any).viewableTracker),
174
289
  });
175
- }
290
+ },
176
291
  };
177
292
  }
178
293
 
@@ -10,7 +10,7 @@ import type { EventProperties } from '../modules/events/events-module';
10
10
  * 이벤트 추적 (메인 함수)
11
11
  * @param eventName 이벤트 이름
12
12
  * @param properties 이벤트 속성
13
- *
13
+ *
14
14
  * @example
15
15
  * track('purchase', {
16
16
  * transaction_id: 'T123',
@@ -23,44 +23,14 @@ export function track(eventName: string, properties?: EventProperties): Promise<
23
23
  console.warn('AdStage not initialized. Call AdStage.init() first.');
24
24
  return Promise.resolve();
25
25
  }
26
-
27
- return AdStage.events.track(eventName, properties);
28
- }
29
-
30
-
31
-
32
- /**
33
- * 사용자 ID 설정
34
- * @param userId 사용자 ID
35
- *
36
- * @example
37
- * setUserId('user123');
38
- */
39
- export function setUserId(userId: string): void {
40
- if (!AdStage.isReady()) {
41
- console.warn('AdStage not initialized. Call AdStage.init() first.');
42
- return;
43
- }
44
-
45
- AdStage.events.setUserId(userId);
46
- }
47
26
 
48
- /**
49
- * 현재 사용자 ID 반환
50
- */
51
- export function getUserId(): string | undefined {
52
- if (!AdStage.isReady()) {
53
- console.warn('AdStage not initialized. Call AdStage.init() first.');
54
- return undefined;
55
- }
56
-
57
- return AdStage.events.getUserId();
27
+ return AdStage.events.track(eventName, properties);
58
28
  }
59
29
 
60
30
  /**
61
31
  * 사용자 속성 설정
62
32
  * @param properties 사용자 속성 객체
63
- *
33
+ *
64
34
  * @example
65
35
  * setUserProperties({
66
36
  * gender: 'male',
@@ -73,7 +43,7 @@ export function setUserProperties(properties: import('../types/events').UserProp
73
43
  console.warn('AdStage not initialized. Call AdStage.init() first.');
74
44
  return;
75
45
  }
76
-
46
+
77
47
  AdStage.events.setUserProperties(properties);
78
48
  }
79
49
 
@@ -85,6 +55,6 @@ export function getUserProperties(): any {
85
55
  console.warn('AdStage not initialized. Call AdStage.init() first.');
86
56
  return undefined;
87
57
  }
88
-
58
+
89
59
  return AdStage.events.getUserProperties();
90
60
  }
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ export { default as AdStage } from './core/adstage';
8
8
  import AdStageCore from './core/adstage';
9
9
 
10
10
  // 전역 이벤트 함수들 (Firebase 스타일)
11
- export { track, setUserId, getUserId, setUserProperties, getUserProperties } from './events/global-events';
11
+ export { track, setUserProperties, getUserProperties } from './events/global-events';
12
12
 
13
13
  // React 통합
14
14
  export { AdStageProvider, useAdStageContext, useAdStageInstance } from './react';
@@ -19,6 +19,8 @@ export type { AdStageConfig, ModuleName, BaseModule, ApiResponse, OrganizationIn
19
19
  // 모듈별 타입
20
20
  export type { AdOptions } from './modules/ads/ads-module';
21
21
  export type { EventProperties, UserProperties } from './modules/events/events-module';
22
+ export type { TrackingParams } from './modules/tracking/tracking-params';
23
+ export { TrackingParamsModule } from './modules/tracking/tracking-params';
22
24
 
23
25
  // 광고 관련 타입
24
26
  export type { AdType, AdEventType, Advertisement, AdSlot } from './types/advertisement';
@@ -9,7 +9,7 @@ import { ApiHeaders } from '../../utils/api-headers';
9
9
  import { getSDKVersion } from '../../utils/version';
10
10
  import { EventDeviceCollector } from '../../managers/events/event-device-collector';
11
11
  import { EventUserCollector } from '../../managers/events/event-user-collector';
12
- import { EventSessionManager } from '../../managers/events/event-session-manager';
12
+ import { TrackingParamsModule } from '../tracking/tracking-params';
13
13
  import type { UserProperties, EventProperties } from '../../types/events';
14
14
 
15
15
  export type { EventProperties, UserProperties };
@@ -44,30 +44,12 @@ export class EventsModule implements BaseModule {
44
44
  return this._config;
45
45
  }
46
46
 
47
- /**
48
- * 사용자 ID 설정
49
- */
50
- setUserId(userId: string): void {
51
- EventSessionManager.setUserId(userId);
52
-
53
- if (this._config?.debug) {
54
- console.log('👤 User ID set:', userId);
55
- }
56
- }
57
-
58
- /**
59
- * 현재 사용자 ID 반환
60
- */
61
- getUserId(): string | undefined {
62
- return EventSessionManager.getUserId();
63
- }
64
-
65
47
  /**
66
48
  * 사용자 속성 설정
67
49
  */
68
50
  setUserProperties(properties: UserProperties): void {
69
51
  EventUserCollector.setUserProperties(properties);
70
-
52
+
71
53
  if (this._config?.debug) {
72
54
  console.log('👤 User properties set:', properties);
73
55
  }
@@ -83,15 +65,17 @@ export class EventsModule implements BaseModule {
83
65
  /**
84
66
  * 디바이스 정보 설정
85
67
  */
86
- setDeviceInfo(deviceInfo: Partial<{
87
- category: 'mobile' | 'desktop' | 'tablet' | 'other';
88
- platform: string;
89
- model: string;
90
- appVersion: string;
91
- osVersion: string;
92
- }>): void {
68
+ setDeviceInfo(
69
+ deviceInfo: Partial<{
70
+ category: 'mobile' | 'desktop' | 'tablet' | 'other';
71
+ platform: string;
72
+ model: string;
73
+ appVersion: string;
74
+ osVersion: string;
75
+ }>,
76
+ ): void {
93
77
  EventDeviceCollector.setDeviceInfo(deviceInfo);
94
-
78
+
95
79
  if (this._config?.debug) {
96
80
  console.log('📱 Device info set:', deviceInfo);
97
81
  }
@@ -102,7 +86,7 @@ export class EventsModule implements BaseModule {
102
86
  */
103
87
  setDeviceProperty(key: 'category' | 'platform' | 'model' | 'appVersion' | 'osVersion', value: string): void {
104
88
  EventDeviceCollector.setDeviceProperty(key, value);
105
-
89
+
106
90
  if (this._config?.debug) {
107
91
  console.log('📱 Device property set:', key, value);
108
92
  }
@@ -113,7 +97,7 @@ export class EventsModule implements BaseModule {
113
97
  */
114
98
  clearDeviceInfo(): void {
115
99
  EventDeviceCollector.clearDeviceInfo();
116
-
100
+
117
101
  if (this._config?.debug) {
118
102
  console.log('📱 User provided device info cleared');
119
103
  }
@@ -164,47 +148,198 @@ export class EventsModule implements BaseModule {
164
148
  }
165
149
 
166
150
  try {
151
+ // 🆔 저장된 Attribution ID 확인
152
+ const attributionId = this.getAttributionId();
153
+
154
+ // Attribution ID가 있으면 서버에서 조회하도록 전달
155
+ if (attributionId) {
156
+ const eventData = {
157
+ eventName,
158
+ sdkVersion: getSDKVersion(),
159
+ device: EventDeviceCollector.getDeviceInfo(),
160
+ user: EventUserCollector.getUserInfo(),
161
+ attributionId, // 서버가 이 ID로 attribution 조회
162
+ params: properties || {},
163
+ };
164
+
165
+ await this.sendEventToServer(eventData);
166
+
167
+ if (this._config.debug) {
168
+ console.log('✅ Event tracked with attribution ID:', eventName, attributionId);
169
+ }
170
+ return;
171
+ }
172
+
173
+ // Attribution ID가 없으면 기존 방식 (URL 파라미터 직접 전달)
174
+ const trackingParams = TrackingParamsModule.toAttributionObject();
175
+
167
176
  const eventData = {
168
177
  eventName,
169
- userId: EventSessionManager.getUserId(),
170
- sessionId: EventSessionManager.getSessionId(),
171
178
  sdkVersion: getSDKVersion(),
172
179
  device: EventDeviceCollector.getDeviceInfo(),
173
180
  user: EventUserCollector.getUserInfo(),
174
- params: properties || {}
181
+
182
+ // attribution 필드에 추적 파라미터 자동 포함
183
+ ...(trackingParams && { attribution: trackingParams }),
184
+
185
+ params: properties || {},
175
186
  };
176
187
 
177
188
  await this.sendEventToServer(eventData);
178
189
 
179
190
  if (this._config.debug) {
180
191
  console.log('✅ Event tracked:', eventName, properties);
192
+ if (trackingParams) {
193
+ console.log('📊 Attribution included:', trackingParams);
194
+ }
181
195
  }
182
196
  } catch (error) {
183
197
  console.error('❌ Failed to track event:', error);
184
-
198
+
185
199
  if (this._config.debug) {
186
200
  console.error('Event data:', { eventName, properties });
187
201
  }
188
202
  }
189
203
  }
190
204
 
205
+ /**
206
+ * Attribution 등록 (서버 기반 Attribution ID 패턴)
207
+ *
208
+ * 사용 사례:
209
+ * 1. 앱 설치 시 Install Referrer 전송
210
+ * 2. 웹 첫 방문 시 URL 파라미터 전송
211
+ *
212
+ * @param deviceId 디바이스 고유 ID
213
+ * @returns Attribution ID 및 파싱된 정보
214
+ */
215
+ async registerAttribution(deviceId: string): Promise<{
216
+ attributionId: string;
217
+ channel: string;
218
+ campaign?: string;
219
+ attribution: Record<string, any>;
220
+ installedAt: Date;
221
+ expiresAt: Date;
222
+ } | null> {
223
+ if (!this._isReady) {
224
+ console.warn('Events module not initialized. Call AdStage.init() first.');
225
+ return null;
226
+ }
227
+
228
+ if (!this._config?.apiKey) {
229
+ console.warn('API key not configured for attribution registration.');
230
+ return null;
231
+ }
232
+
233
+ try {
234
+ // 현재 URL의 추적 파라미터 가져오기
235
+ const trackingParams = TrackingParamsModule.get();
236
+
237
+ if (!trackingParams || Object.keys(trackingParams).length === 0) {
238
+ if (this._config.debug) {
239
+ console.log('⏭️ No tracking parameters found, skipping attribution registration');
240
+ }
241
+ return null;
242
+ }
243
+
244
+ // 서버에 Attribution 등록
245
+ const response = await this.sendAttributionToServer({
246
+ deviceId,
247
+ referrerParams: trackingParams,
248
+ });
249
+
250
+ if (this._config.debug) {
251
+ console.log('✅ Attribution registered:', response);
252
+ }
253
+
254
+ // Attribution ID를 sessionStorage에 저장
255
+ if (response.attributionId) {
256
+ sessionStorage.setItem('adstage_attribution_id', response.attributionId);
257
+ }
258
+
259
+ return response;
260
+ } catch (error) {
261
+ console.error('❌ Failed to register attribution:', error);
262
+ return null;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * 저장된 Attribution ID 반환
268
+ */
269
+ getAttributionId(): string | null {
270
+ if (typeof window === 'undefined' || !window.sessionStorage) {
271
+ return null;
272
+ }
191
273
 
274
+ return sessionStorage.getItem('adstage_attribution_id');
275
+ }
276
+
277
+ /**
278
+ * Attribution ID로 이벤트 전송 (별칭 메서드)
279
+ * track() 메서드가 자동으로 Attribution ID를 사용하므로 동일하게 동작
280
+ * @param eventName 이벤트 이름
281
+ * @param properties 이벤트 속성
282
+ */
283
+ async trackWithAttributionId(eventName: string, properties?: EventProperties): Promise<void> {
284
+ // track() 메서드로 위임 (자동으로 Attribution ID 사용)
285
+ return this.track(eventName, properties);
286
+ }
192
287
 
193
288
  /**
194
289
  * 서버에 이벤트 전송
195
290
  */
196
291
  private async sendEventToServer(eventData: any): Promise<void> {
292
+ const headers = {
293
+ ...ApiHeaders.create(this._config!.apiKey),
294
+ 'Content-Type': 'application/json',
295
+ };
296
+
297
+ if (this._config?.debug) {
298
+ console.log('🔑 Request headers:', headers);
299
+ console.log('📡 Request URL:', endpoints.events.track());
300
+ console.log('📦 Request body:', eventData);
301
+ }
302
+
197
303
  const response = await fetch(endpoints.events.track(), {
198
304
  method: 'POST',
199
- headers: {
200
- ...ApiHeaders.create(this._config!.apiKey),
201
- 'Content-Type': 'application/json'
202
- },
203
- body: JSON.stringify(eventData)
305
+ headers,
306
+ body: JSON.stringify(eventData),
204
307
  });
205
308
 
206
309
  if (!response.ok) {
310
+ const errorText = await response.text();
311
+ console.error('❌ API Error Response:', errorText);
207
312
  throw new Error(`Event tracking failed: ${response.status} ${response.statusText}`);
208
313
  }
209
314
  }
315
+
316
+ /**
317
+ * 서버에 Attribution 등록
318
+ */
319
+ private async sendAttributionToServer(data: { deviceId: string; referrerParams: Record<string, any> }): Promise<any> {
320
+ const headers = {
321
+ ...ApiHeaders.create(this._config!.apiKey),
322
+ 'Content-Type': 'application/json',
323
+ };
324
+
325
+ if (this._config?.debug) {
326
+ console.log('🔑 Attribution request headers:', headers);
327
+ console.log('📡 Attribution request URL:', endpoints.events.registerAttribution());
328
+ console.log('📦 Attribution request body:', data);
329
+ }
330
+
331
+ const response = await fetch(endpoints.events.registerAttribution(), {
332
+ method: 'POST',
333
+ headers,
334
+ body: JSON.stringify(data),
335
+ });
336
+
337
+ if (!response.ok) {
338
+ const errorText = await response.text();
339
+ console.error('❌ API Error Response:', errorText);
340
+ throw new Error(`Attribution registration failed: ${response.status} ${response.statusText}`);
341
+ }
342
+
343
+ return response.json();
344
+ }
210
345
  }