@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.
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +144 -21
- package/dist/index.esm.js +1 -1
- package/dist/index.standalone.js +2 -2
- package/dist/index.umd.js +2 -2
- package/package.json +1 -1
- package/src/constants/endpoints.ts +8 -7
- package/src/core/adstage.ts +187 -7
- package/src/events/global-events.ts +5 -35
- package/src/index.ts +3 -1
- package/src/modules/events/events-module.ts +185 -39
- package/src/modules/tracking/tracking-params.ts +318 -0
- package/src/types/events.ts +0 -2
- package/src/managers/events/event-session-manager.ts +0 -183
package/package.json
CHANGED
|
@@ -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
|
/**
|
package/src/core/adstage.ts
CHANGED
|
@@ -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,151 @@ 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
|
+
// 🔎 리다이렉트 단계에서 전달된 attributionId(attribution_id) 우선 사용
|
|
82
|
+
const aidFromUrl = (() => {
|
|
83
|
+
try {
|
|
84
|
+
const search = new URLSearchParams(window.location.search);
|
|
85
|
+
const hash = new URLSearchParams((window.location.hash || '').replace(/^#/, ''));
|
|
86
|
+
return search.get('attribution_id') || hash.get('attribution_id');
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
|
|
92
|
+
if (aidFromUrl) {
|
|
93
|
+
try {
|
|
94
|
+
localStorage.setItem('adstage_attribution_id', aidFromUrl);
|
|
95
|
+
if (config.debug) {
|
|
96
|
+
console.log('🔗 Using attributionId from URL (attribution_id):', aidFromUrl);
|
|
97
|
+
}
|
|
98
|
+
} catch (_) {
|
|
99
|
+
// no-op: storage might be unavailable in some contexts
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 🎯 Click 이벤트 자동 전송 (새로운 클릭 ID 발견 시)
|
|
104
|
+
const shouldSend = TrackingParamsModule.shouldSendClickEvent();
|
|
105
|
+
if (config.debug) {
|
|
106
|
+
console.log('🔍 Should send click event:', shouldSend);
|
|
107
|
+
if (!shouldSend) {
|
|
108
|
+
console.log('⏭️ Skipping click event (duplicate or no click ID)');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// � 서버에 Attribution 자동 등록 (클릭 ID가 있는 경우, 단 URL로 이미 전달된 경우는 생략)
|
|
113
|
+
if (!aidFromUrl && trackingParams && TrackingParamsModule.hasClickId()) {
|
|
114
|
+
// Attribution 등록을 Promise로 처리
|
|
115
|
+
const attributionPromise = (async () => {
|
|
116
|
+
try {
|
|
117
|
+
// Device ID 생성 (브라우저 fingerprint 또는 랜덤)
|
|
118
|
+
const deviceId = instance.generateDeviceId();
|
|
119
|
+
|
|
120
|
+
const attribution = await instance.events.registerAttribution(deviceId);
|
|
121
|
+
|
|
122
|
+
if (attribution && config.debug) {
|
|
123
|
+
console.log('✅ Attribution registered:', {
|
|
124
|
+
attributionId: attribution.attributionId,
|
|
125
|
+
channel: attribution.channel,
|
|
126
|
+
campaign: attribution.campaign,
|
|
127
|
+
expiresAt: attribution.expiresAt,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return attribution;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error('❌ Failed to register attribution:', err);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
|
|
138
|
+
// Click 이벤트는 Attribution 등록이 완료된 후에만 전송
|
|
139
|
+
if (shouldSend) {
|
|
140
|
+
attributionPromise.then((attribution) => {
|
|
141
|
+
try {
|
|
142
|
+
const trackingParams = TrackingParamsModule.get();
|
|
143
|
+
const attributionObj = TrackingParamsModule.toAttributionObject();
|
|
144
|
+
|
|
145
|
+
if (config.debug) {
|
|
146
|
+
console.log('🎯 Sending click event after attribution registration', {
|
|
147
|
+
attributionId: attribution?.attributionId,
|
|
148
|
+
hasAttribution: !!attribution,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Click 이벤트 전송 (추적 파라미터는 EventsModule에서 자동 주입)
|
|
153
|
+
instance.events
|
|
154
|
+
.track('click', {
|
|
155
|
+
_autoGenerated: true,
|
|
156
|
+
_source: 'tracking_params',
|
|
157
|
+
_timestamp: new Date().toISOString(),
|
|
158
|
+
_platform: 'web',
|
|
159
|
+
})
|
|
160
|
+
.then(() => {
|
|
161
|
+
TrackingParamsModule.markClickEventSent();
|
|
162
|
+
|
|
163
|
+
if (config.debug) {
|
|
164
|
+
console.log('✅ Auto-tracked click event with tracking params:', {
|
|
165
|
+
attribution: attributionObj,
|
|
166
|
+
trackingParams,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
.catch((err) => {
|
|
171
|
+
console.error('❌ Failed to auto-track click event:', err);
|
|
172
|
+
});
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error('❌ Error in click event auto-tracking:', err);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} else if (shouldSend) {
|
|
179
|
+
// Attribution 등록이 필요없는 경우 (aidFromUrl이 있거나 클릭 ID가 없음)
|
|
180
|
+
// 기존 방식대로 Click 이벤트만 전송
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
try {
|
|
183
|
+
const trackingParams = TrackingParamsModule.get();
|
|
184
|
+
const attribution = TrackingParamsModule.toAttributionObject();
|
|
185
|
+
|
|
186
|
+
instance.events
|
|
187
|
+
.track('click', {
|
|
188
|
+
_autoGenerated: true,
|
|
189
|
+
_source: 'tracking_params',
|
|
190
|
+
_timestamp: new Date().toISOString(),
|
|
191
|
+
_platform: 'web',
|
|
192
|
+
})
|
|
193
|
+
.then(() => {
|
|
194
|
+
TrackingParamsModule.markClickEventSent();
|
|
195
|
+
|
|
196
|
+
if (config.debug) {
|
|
197
|
+
console.log('✅ Auto-tracked click event with tracking params:', {
|
|
198
|
+
attribution,
|
|
199
|
+
trackingParams,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
.catch((err) => {
|
|
204
|
+
console.error('❌ Failed to auto-track click event:', err);
|
|
205
|
+
});
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.error('❌ Error in click event auto-tracking:', err);
|
|
208
|
+
}
|
|
209
|
+
}, 100);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
73
213
|
if (config.debug) {
|
|
74
214
|
console.log('🚀 AdStage SDK initialized (sync mode)', {
|
|
75
215
|
version: '2.0.0',
|
|
76
216
|
modules: enabledModules,
|
|
77
217
|
apiKey: config.apiKey.substring(0, 8) + '...',
|
|
78
|
-
mode: 'development'
|
|
218
|
+
mode: 'development',
|
|
79
219
|
});
|
|
80
220
|
}
|
|
81
221
|
}
|
|
@@ -144,6 +284,46 @@ export class AdStage {
|
|
|
144
284
|
}
|
|
145
285
|
}
|
|
146
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Device ID 생성 (브라우저 fingerprint 기반)
|
|
289
|
+
* localStorage에 저장하여 재사용
|
|
290
|
+
*/
|
|
291
|
+
private generateDeviceId(): string {
|
|
292
|
+
if (typeof window === 'undefined') {
|
|
293
|
+
return 'server-' + Math.random().toString(36).substring(2, 15);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 기존 Device ID 확인
|
|
297
|
+
const existingId = localStorage.getItem('adstage_device_id');
|
|
298
|
+
if (existingId) {
|
|
299
|
+
return existingId;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 새 Device ID 생성 (브라우저 fingerprint 기반)
|
|
303
|
+
const fingerprint = [
|
|
304
|
+
navigator.userAgent,
|
|
305
|
+
navigator.language,
|
|
306
|
+
screen.width + 'x' + screen.height,
|
|
307
|
+
new Date().getTimezoneOffset(),
|
|
308
|
+
navigator.hardwareConcurrency || 0,
|
|
309
|
+
].join('|');
|
|
310
|
+
|
|
311
|
+
// 간단한 해시 생성
|
|
312
|
+
let hash = 0;
|
|
313
|
+
for (let i = 0; i < fingerprint.length; i++) {
|
|
314
|
+
const char = fingerprint.charCodeAt(i);
|
|
315
|
+
hash = (hash << 5) - hash + char;
|
|
316
|
+
hash = hash & hash;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const deviceId = 'web-' + Math.abs(hash).toString(36) + '-' + Date.now().toString(36);
|
|
320
|
+
|
|
321
|
+
// localStorage에 저장
|
|
322
|
+
localStorage.setItem('adstage_device_id', deviceId);
|
|
323
|
+
|
|
324
|
+
return deviceId;
|
|
325
|
+
}
|
|
326
|
+
|
|
147
327
|
/**
|
|
148
328
|
* 디버그용 메서드들
|
|
149
329
|
*/
|
|
@@ -170,9 +350,9 @@ export class AdStage {
|
|
|
170
350
|
getViewableStatus: (): void => {
|
|
171
351
|
console.log('📊 AdStage Debug: 현재 viewable 추적 상태', {
|
|
172
352
|
trackedCount: (ViewableEventTracker as any).viewableTracker.size,
|
|
173
|
-
trackedItems: Array.from((ViewableEventTracker as any).viewableTracker)
|
|
353
|
+
trackedItems: Array.from((ViewableEventTracker as any).viewableTracker),
|
|
174
354
|
});
|
|
175
|
-
}
|
|
355
|
+
},
|
|
176
356
|
};
|
|
177
357
|
}
|
|
178
358
|
|
|
@@ -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,
|
|
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 {
|
|
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(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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,209 @@ 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
|
-
|
|
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를 localStorage + sessionStorage에 저장 (세션 종료 후에도 유지)
|
|
255
|
+
if (response.attributionId) {
|
|
256
|
+
try { localStorage.setItem('adstage_attribution_id', response.attributionId); } catch (_) {}
|
|
257
|
+
try { sessionStorage.setItem('adstage_attribution_id', response.attributionId); } catch (_) {}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return response;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('❌ Failed to register attribution:', error);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 저장된 Attribution ID 반환
|
|
269
|
+
* 우선순위: sessionStorage (현재 세션) > localStorage (영속 저장)
|
|
270
|
+
*/
|
|
271
|
+
getAttributionId(): string | null {
|
|
272
|
+
if (typeof window === 'undefined') {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const fromSession = sessionStorage.getItem('adstage_attribution_id');
|
|
278
|
+
if (fromSession) return fromSession;
|
|
279
|
+
} catch (_) {}
|
|
191
280
|
|
|
281
|
+
try {
|
|
282
|
+
return localStorage.getItem('adstage_attribution_id');
|
|
283
|
+
} catch (_) {}
|
|
284
|
+
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Attribution ID로 이벤트 전송 (별칭 메서드)
|
|
290
|
+
* track() 메서드가 자동으로 Attribution ID를 사용하므로 동일하게 동작
|
|
291
|
+
* @param eventName 이벤트 이름
|
|
292
|
+
* @param properties 이벤트 속성
|
|
293
|
+
*/
|
|
294
|
+
async trackWithAttributionId(eventName: string, properties?: EventProperties): Promise<void> {
|
|
295
|
+
// track() 메서드로 위임 (자동으로 Attribution ID 사용)
|
|
296
|
+
return this.track(eventName, properties);
|
|
297
|
+
}
|
|
192
298
|
|
|
193
299
|
/**
|
|
194
300
|
* 서버에 이벤트 전송
|
|
195
301
|
*/
|
|
196
302
|
private async sendEventToServer(eventData: any): Promise<void> {
|
|
303
|
+
const headers = {
|
|
304
|
+
...ApiHeaders.create(this._config!.apiKey),
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (this._config?.debug) {
|
|
309
|
+
console.log('🔑 Request headers:', headers);
|
|
310
|
+
console.log('📡 Request URL:', endpoints.events.track());
|
|
311
|
+
console.log('📦 Request body:', eventData);
|
|
312
|
+
}
|
|
313
|
+
|
|
197
314
|
const response = await fetch(endpoints.events.track(), {
|
|
198
315
|
method: 'POST',
|
|
199
|
-
headers
|
|
200
|
-
|
|
201
|
-
'Content-Type': 'application/json'
|
|
202
|
-
},
|
|
203
|
-
body: JSON.stringify(eventData)
|
|
316
|
+
headers,
|
|
317
|
+
body: JSON.stringify(eventData),
|
|
204
318
|
});
|
|
205
319
|
|
|
206
320
|
if (!response.ok) {
|
|
321
|
+
const errorText = await response.text();
|
|
322
|
+
console.error('❌ API Error Response:', errorText);
|
|
207
323
|
throw new Error(`Event tracking failed: ${response.status} ${response.statusText}`);
|
|
208
324
|
}
|
|
209
325
|
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 서버에 Attribution 등록
|
|
329
|
+
*/
|
|
330
|
+
private async sendAttributionToServer(data: { deviceId: string; referrerParams: Record<string, any> }): Promise<any> {
|
|
331
|
+
const headers = {
|
|
332
|
+
...ApiHeaders.create(this._config!.apiKey),
|
|
333
|
+
'Content-Type': 'application/json',
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
if (this._config?.debug) {
|
|
337
|
+
console.log('🔑 Attribution request headers:', headers);
|
|
338
|
+
console.log('📡 Attribution request URL:', endpoints.events.registerAttribution());
|
|
339
|
+
console.log('📦 Attribution request body:', data);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const response = await fetch(endpoints.events.registerAttribution(), {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
headers,
|
|
345
|
+
body: JSON.stringify(data),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
const errorText = await response.text();
|
|
350
|
+
console.error('❌ API Error Response:', errorText);
|
|
351
|
+
throw new Error(`Attribution registration failed: ${response.status} ${response.statusText}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return response.json();
|
|
355
|
+
}
|
|
210
356
|
}
|