@adstage/web-sdk 2.4.12 → 2.5.1
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 +107 -461
- package/dist/index.d.ts +3 -27
- package/dist/index.esm.js +107 -461
- package/dist/index.standalone.js +107 -461
- package/package.json +1 -1
- package/src/dummy/ads_dummy.json +84 -0
- package/src/managers/ads/advertisement-event-tracker.ts +27 -36
- package/src/managers/ads/carousel-slider-manager.ts +8 -5
- package/src/managers/ads/viewability-tracker.ts +16 -140
- package/src/managers/ads/viewable-event-tracker.ts +10 -82
- package/src/modules/ads/AdRenderer.ts +1 -1
- package/src/modules/ads/AdsModule.ts +16 -89
- package/src/utils/version.ts +23 -0
- package/src/managers/ads/basic-fraud-detector.ts +0 -191
|
@@ -8,9 +8,7 @@ import { AdType, AdEventType } from '../../types/advertisement';
|
|
|
8
8
|
import type { AdSlot, Advertisement } from '../../types/advertisement';
|
|
9
9
|
// 렌더링 관련 매니저/뷰 트래커는 AdRenderer로 이전됨
|
|
10
10
|
import { AdvertisementEventTracker } from '../../managers/ads/advertisement-event-tracker';
|
|
11
|
-
import {
|
|
12
|
-
import { BasicFraudDetector } from '../../managers/ads/basic-fraud-detector';
|
|
13
|
-
import type { ViewabilityMetrics } from '../../managers/ads/viewability-tracker';
|
|
11
|
+
import { SimpleViewabilityTracker } from '../../managers/ads/viewability-tracker';
|
|
14
12
|
import { endpoints } from '../../constants/endpoints';
|
|
15
13
|
import { ApiHeaders } from '../../utils/api-headers';
|
|
16
14
|
// 새로 분리된 클래스들
|
|
@@ -190,16 +188,11 @@ export class AdsModule implements BaseModule {
|
|
|
190
188
|
throw new Error(`Ad slot not found: ${slotId}`);
|
|
191
189
|
}
|
|
192
190
|
|
|
193
|
-
//
|
|
191
|
+
// SimpleViewabilityTracker 정리
|
|
194
192
|
if ((slot as any).viewabilityTracker) {
|
|
195
193
|
(slot as any).viewabilityTracker.destroy();
|
|
196
194
|
}
|
|
197
195
|
|
|
198
|
-
// BasicFraudDetector 정리
|
|
199
|
-
if ((slot as any).fraudDetector) {
|
|
200
|
-
(slot as any).fraudDetector.destroy();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
196
|
// DOM에서 제거
|
|
204
197
|
const container = document.getElementById(slot.containerId);
|
|
205
198
|
if (container) {
|
|
@@ -283,23 +276,6 @@ export class AdsModule implements BaseModule {
|
|
|
283
276
|
return slotId;
|
|
284
277
|
}
|
|
285
278
|
|
|
286
|
-
// createAdSlot 제거: AdRenderer.createPlaceholder 사용
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* 여러 광고의 최적 컨테이너 크기 계산 (동적 크기 조정)
|
|
290
|
-
*/
|
|
291
|
-
// calculateOptimalContainerSize 제거: AdRenderer.calculateOptimalContainerSize 사용
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* 최적 크기 조정 전략 선택
|
|
295
|
-
*/
|
|
296
|
-
// selectOptimalSizeStrategy 제거: AdRenderer 내부 구현 사용
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* 전략에 따른 최적 높이 계산
|
|
300
|
-
*/
|
|
301
|
-
// calculateOptimalHeight 제거: AdRenderer 내부 구현 사용
|
|
302
|
-
|
|
303
279
|
/**
|
|
304
280
|
* 백그라운드에서 광고 콘텐츠 로드
|
|
305
281
|
*/
|
|
@@ -322,11 +298,9 @@ export class AdsModule implements BaseModule {
|
|
|
322
298
|
if (adstageData.length > 1 || (slot.config as any)?.autoSlide) {
|
|
323
299
|
await this.adRenderer?.renderAdSlider(slot, adstageData);
|
|
324
300
|
|
|
325
|
-
// 🔧
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
this.startBasicViewabilityTracking(slot, adstageData[0]);
|
|
329
|
-
}, 100); // 슬라이더 렌더링 완료 후 추적 시작
|
|
301
|
+
// 🔧 슬라이더는 CarouselSliderManager에서 자체적으로 모든 광고의 VIEWABLE 이벤트를 처리
|
|
302
|
+
if (this._config?.debug) {
|
|
303
|
+
console.log(`🎠 Slider will handle VIEWABLE events for ${adstageData.length} ads automatically`);
|
|
330
304
|
}
|
|
331
305
|
} else {
|
|
332
306
|
// 광고가 1개면 일반 렌더링
|
|
@@ -334,7 +308,7 @@ export class AdsModule implements BaseModule {
|
|
|
334
308
|
await this.adRenderer?.renderAdElement(slot, adstageData[0]);
|
|
335
309
|
|
|
336
310
|
// ✅ 신규: Viewable impression 추적 시작 (기존 즉시 추적 대신)
|
|
337
|
-
this.
|
|
311
|
+
this.startSimpleViewabilityTracking(slot, adstageData[0]);
|
|
338
312
|
}
|
|
339
313
|
|
|
340
314
|
slot.isLoaded = true;
|
|
@@ -354,9 +328,9 @@ export class AdsModule implements BaseModule {
|
|
|
354
328
|
// optimizeContainerForBannerAds 제거: AdRenderer.optimizeContainerForBannerAds 사용
|
|
355
329
|
|
|
356
330
|
/**
|
|
357
|
-
*
|
|
331
|
+
* 단순한 노출 추적 시작 (재시도 로직 포함)
|
|
358
332
|
*/
|
|
359
|
-
private
|
|
333
|
+
private startSimpleViewabilityTracking(slot: AdSlot, ad: Advertisement): void {
|
|
360
334
|
const tryStartTracking = (retryCount = 0) => {
|
|
361
335
|
const element = document.getElementById(slot.id);
|
|
362
336
|
|
|
@@ -373,20 +347,16 @@ export class AdsModule implements BaseModule {
|
|
|
373
347
|
return;
|
|
374
348
|
}
|
|
375
349
|
|
|
376
|
-
//
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
// viewability 추적
|
|
380
|
-
const tracker = new ViewabilityTracker(element, slot.adType, async (metrics) => {
|
|
381
|
-
await this.handleViewableEvent(ad, slot, metrics, fraudDetector);
|
|
350
|
+
// 단순한 노출 추적
|
|
351
|
+
const tracker = new SimpleViewabilityTracker(element, async () => {
|
|
352
|
+
await this.handleViewableEvent(ad, slot);
|
|
382
353
|
});
|
|
383
354
|
|
|
384
355
|
// 정리를 위해 저장
|
|
385
356
|
(slot as any).viewabilityTracker = tracker;
|
|
386
|
-
(slot as any).fraudDetector = fraudDetector;
|
|
387
357
|
|
|
388
358
|
if (this._config?.debug) {
|
|
389
|
-
console.log(`🎯
|
|
359
|
+
console.log(`🎯 Simple viewability tracking started for slot: ${slot.id} (element found)`);
|
|
390
360
|
}
|
|
391
361
|
};
|
|
392
362
|
|
|
@@ -394,41 +364,23 @@ export class AdsModule implements BaseModule {
|
|
|
394
364
|
}
|
|
395
365
|
|
|
396
366
|
/**
|
|
397
|
-
* Viewable 이벤트 처리
|
|
367
|
+
* Viewable 이벤트 처리 (단순화됨)
|
|
398
368
|
*/
|
|
399
369
|
private async handleViewableEvent(
|
|
400
370
|
ad: Advertisement,
|
|
401
|
-
slot: AdSlot
|
|
402
|
-
metrics: ViewabilityMetrics,
|
|
403
|
-
fraudDetector: BasicFraudDetector
|
|
371
|
+
slot: AdSlot
|
|
404
372
|
): Promise<void> {
|
|
405
373
|
try {
|
|
406
|
-
const fraudScore = fraudDetector.calculateFraudScore();
|
|
407
|
-
|
|
408
|
-
// 높은 위험도면 차단
|
|
409
|
-
if (fraudScore.riskLevel === 'CRITICAL') {
|
|
410
|
-
if (this._config?.debug) {
|
|
411
|
-
console.warn(`🚫 Viewable blocked due to fraud risk: ${fraudScore.score}`, fraudScore.reasons);
|
|
412
|
-
}
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
374
|
// VIEWABLE 이벤트 전송
|
|
417
375
|
if (this.advertisementEventTracker) {
|
|
418
376
|
await this.advertisementEventTracker.trackAdvertisementEvent(
|
|
419
377
|
ad._id,
|
|
420
378
|
slot.id,
|
|
421
|
-
AdEventType.VIEWABLE
|
|
422
|
-
{
|
|
423
|
-
viewabilityMetrics: metrics,
|
|
424
|
-
fraudScore: fraudScore.score,
|
|
425
|
-
fraudReasons: fraudScore.reasons,
|
|
426
|
-
riskLevel: fraudScore.riskLevel
|
|
427
|
-
}
|
|
379
|
+
AdEventType.VIEWABLE
|
|
428
380
|
);
|
|
429
381
|
|
|
430
382
|
if (this._config?.debug) {
|
|
431
|
-
console.log(`✅
|
|
383
|
+
console.log(`✅ Simple viewable impression tracked for ad ${ad._id}`);
|
|
432
384
|
}
|
|
433
385
|
}
|
|
434
386
|
|
|
@@ -437,16 +389,6 @@ export class AdsModule implements BaseModule {
|
|
|
437
389
|
}
|
|
438
390
|
}
|
|
439
391
|
|
|
440
|
-
/**
|
|
441
|
-
* Fallback 광고 렌더링 - AdStage 확실한 컨테이너 우선 탐지
|
|
442
|
-
*/
|
|
443
|
-
// renderFallback 제거: AdRenderer.renderFallback 사용
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* 빈 컨테이너 생성 (컨테이너를 찾지 못한 경우)
|
|
447
|
-
*/
|
|
448
|
-
// createEmptyContainer 제거: AdRenderer 내부 구현 사용
|
|
449
|
-
|
|
450
392
|
/**
|
|
451
393
|
* 광고 데이터 가져오기
|
|
452
394
|
*/
|
|
@@ -523,21 +465,6 @@ export class AdsModule implements BaseModule {
|
|
|
523
465
|
return advertisements;
|
|
524
466
|
}
|
|
525
467
|
|
|
526
|
-
/**
|
|
527
|
-
* 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
|
|
528
|
-
*/
|
|
529
|
-
// renderAdSlider 제거: AdRenderer.renderAdSlider 사용
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* 광고 렌더링 (단일 광고용)
|
|
533
|
-
*/
|
|
534
|
-
// renderAd 제거: AdRenderer.renderAd 사용
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* 광고 요소 렌더링 (기본 구현)
|
|
538
|
-
*/
|
|
539
|
-
// renderAdElement 제거: AdRenderer.renderAdElement 사용
|
|
540
|
-
|
|
541
468
|
/**
|
|
542
469
|
* 광고 슬롯 새로고침
|
|
543
470
|
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdStage SDK - 버전 정보 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// package.json에서 버전 정보 가져오기 (빌드 시 자동으로 교체됨)
|
|
6
|
+
export const SDK_VERSION = '__SDK_VERSION__';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SDK 버전 정보 반환
|
|
10
|
+
*/
|
|
11
|
+
export function getSDKVersion(): string {
|
|
12
|
+
return SDK_VERSION;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 버전 정보가 포함된 User-Agent 문자열 생성
|
|
17
|
+
*/
|
|
18
|
+
export function getSDKUserAgent(): string {
|
|
19
|
+
if (typeof navigator !== 'undefined') {
|
|
20
|
+
return `${navigator.userAgent} AdStageSDK/${SDK_VERSION}`;
|
|
21
|
+
}
|
|
22
|
+
return `AdStageSDK/${SDK_VERSION}`;
|
|
23
|
+
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BasicFraudDetector - 현실적 구현 버전
|
|
3
|
-
* 기본적인 봇 탐지 및 간단한 행동 패턴 분석
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface FraudScore {
|
|
7
|
-
score: number; // 0-100
|
|
8
|
-
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
|
9
|
-
reasons: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface BasicBrowserInfo {
|
|
13
|
-
userAgent: string;
|
|
14
|
-
language: string;
|
|
15
|
-
platform: string;
|
|
16
|
-
cookieEnabled: boolean;
|
|
17
|
-
doNotTrack: string | null;
|
|
18
|
-
timezone: string;
|
|
19
|
-
screenResolution: string;
|
|
20
|
-
viewportSize: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class BasicFraudDetector {
|
|
24
|
-
private startTime: number;
|
|
25
|
-
private mouseEvents: number = 0;
|
|
26
|
-
private keyboardEvents: number = 0;
|
|
27
|
-
private scrollEvents: number = 0;
|
|
28
|
-
|
|
29
|
-
constructor() {
|
|
30
|
-
this.startTime = Date.now();
|
|
31
|
-
this.initBasicTracking();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private initBasicTracking(): void {
|
|
35
|
-
// 기본적인 사용자 상호작용 추적
|
|
36
|
-
document.addEventListener('mousemove', () => this.mouseEvents++, { passive: true });
|
|
37
|
-
document.addEventListener('keydown', () => this.keyboardEvents++, { passive: true });
|
|
38
|
-
document.addEventListener('scroll', () => this.scrollEvents++, { passive: true });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
public calculateFraudScore(): FraudScore {
|
|
42
|
-
let score = 0;
|
|
43
|
-
const reasons: string[] = [];
|
|
44
|
-
|
|
45
|
-
// 1. 웹드라이버 탐지 (기본)
|
|
46
|
-
if (this.detectWebDriver()) {
|
|
47
|
-
score += 50;
|
|
48
|
-
reasons.push('WebDriver detected');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 2. 헤드리스 브라우저 기본 탐지
|
|
52
|
-
if (this.detectBasicHeadless()) {
|
|
53
|
-
score += 40;
|
|
54
|
-
reasons.push('Headless browser signatures');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// 3. 사용자 상호작용 부족
|
|
58
|
-
const sessionTime = Date.now() - this.startTime;
|
|
59
|
-
if (sessionTime > 5000) { // 5초 이상 경과
|
|
60
|
-
if (this.mouseEvents === 0) {
|
|
61
|
-
score += 20;
|
|
62
|
-
reasons.push('No mouse interaction');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (this.scrollEvents === 0 && sessionTime > 10000) {
|
|
66
|
-
score += 15;
|
|
67
|
-
reasons.push('No scroll activity');
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 4. 브라우저 환경 이상 징후
|
|
72
|
-
const browserCheck = this.checkBrowserEnvironment();
|
|
73
|
-
score += browserCheck.score;
|
|
74
|
-
reasons.push(...browserCheck.reasons);
|
|
75
|
-
|
|
76
|
-
// 5. 시간 패턴 이상 (너무 빠른 페이지 로드 후 즉시 클릭)
|
|
77
|
-
if (sessionTime < 1000) {
|
|
78
|
-
score += 25;
|
|
79
|
-
reasons.push('Suspiciously fast interaction');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const finalScore = Math.min(score, 100);
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
score: finalScore,
|
|
86
|
-
riskLevel: this.getRiskLevel(finalScore),
|
|
87
|
-
reasons: reasons
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private detectWebDriver(): boolean {
|
|
92
|
-
// 기본적인 웹드라이버 탐지
|
|
93
|
-
return !!(
|
|
94
|
-
(window as any).webdriver ||
|
|
95
|
-
(navigator as any).webdriver ||
|
|
96
|
-
(window as any).__webdriver_evaluate ||
|
|
97
|
-
(window as any).__selenium_evaluate ||
|
|
98
|
-
(window as any).__webdriver_script_function ||
|
|
99
|
-
(window as any).__webdriver_script_func ||
|
|
100
|
-
(window as any).__webdriver_script_fn ||
|
|
101
|
-
(window as any).__fxdriver_evaluate ||
|
|
102
|
-
(window as any).__driver_unwrapped ||
|
|
103
|
-
(window as any).__webdriver_unwrapped ||
|
|
104
|
-
(window as any).__driver_evaluate ||
|
|
105
|
-
(window as any).__selenium_unwrapped ||
|
|
106
|
-
(window as any).__fxdriver_unwrapped
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private detectBasicHeadless(): boolean {
|
|
111
|
-
const signatures: string[] = [];
|
|
112
|
-
|
|
113
|
-
// PhantomJS 탐지
|
|
114
|
-
if ((window as any)._phantom || (window as any).phantom) {
|
|
115
|
-
signatures.push('PhantomJS');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Chrome headless 기본 탐지
|
|
119
|
-
if (navigator.userAgent.includes('HeadlessChrome')) {
|
|
120
|
-
signatures.push('Chrome Headless');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 플러그인 없음 (일반적이지 않음)
|
|
124
|
-
if (navigator.plugins.length === 0) {
|
|
125
|
-
signatures.push('No plugins');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return signatures.length > 0;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private checkBrowserEnvironment(): { score: number; reasons: string[] } {
|
|
132
|
-
let score = 0;
|
|
133
|
-
const reasons: string[] = [];
|
|
134
|
-
|
|
135
|
-
// 언어 설정 이상
|
|
136
|
-
if (!navigator.language || navigator.language === 'C') {
|
|
137
|
-
score += 10;
|
|
138
|
-
reasons.push('Unusual language setting');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// 쿠키 비활성화
|
|
142
|
-
if (!navigator.cookieEnabled) {
|
|
143
|
-
score += 15;
|
|
144
|
-
reasons.push('Cookies disabled');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 화면 해상도 이상
|
|
148
|
-
if (screen.width === 0 || screen.height === 0) {
|
|
149
|
-
score += 20;
|
|
150
|
-
reasons.push('Invalid screen resolution');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 시간대 정보 없음
|
|
154
|
-
try {
|
|
155
|
-
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
156
|
-
if (!timezone || timezone === 'UTC') {
|
|
157
|
-
score += 5;
|
|
158
|
-
reasons.push('No timezone info');
|
|
159
|
-
}
|
|
160
|
-
} catch (e) {
|
|
161
|
-
score += 10;
|
|
162
|
-
reasons.push('Timezone detection failed');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return { score, reasons };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private getRiskLevel(score: number): 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' {
|
|
169
|
-
if (score >= 70) return 'CRITICAL';
|
|
170
|
-
if (score >= 50) return 'HIGH';
|
|
171
|
-
if (score >= 30) return 'MEDIUM';
|
|
172
|
-
return 'LOW';
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
public getBrowserInfo(): BasicBrowserInfo {
|
|
176
|
-
return {
|
|
177
|
-
userAgent: navigator.userAgent,
|
|
178
|
-
language: navigator.language,
|
|
179
|
-
platform: navigator.platform,
|
|
180
|
-
cookieEnabled: navigator.cookieEnabled,
|
|
181
|
-
doNotTrack: navigator.doNotTrack,
|
|
182
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
183
|
-
screenResolution: `${screen.width}x${screen.height}`,
|
|
184
|
-
viewportSize: `${window.innerWidth}x${window.innerHeight}`
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
public destroy(): void {
|
|
189
|
-
// 이벤트 리스너 정리는 생략 (메모리 누수 방지를 위해 필요시 구현)
|
|
190
|
-
}
|
|
191
|
-
}
|