@thead-vantage/react 2.14.0 → 2.16.0
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/README.md +12 -5
- package/package.json +1 -1
- package/src/components/AdBanner.tsx +2 -2
- package/src/components/AdDisplay.tsx +13 -2
- package/src/index.ts +2 -0
- package/src/lib/ads.ts +99 -8
package/README.md
CHANGED
|
@@ -184,7 +184,7 @@ export default function ArticlePage() {
|
|
|
184
184
|
You can also use the utility functions directly:
|
|
185
185
|
|
|
186
186
|
```tsx
|
|
187
|
-
import { fetchAdBanner, trackImpression, trackClick } from '@/lib/ads';
|
|
187
|
+
import { fetchAdBanner, trackImpression, trackClick, collectFingerprint } from '@/lib/ads';
|
|
188
188
|
|
|
189
189
|
// Fetch an ad
|
|
190
190
|
const response = await fetchAdBanner({
|
|
@@ -196,8 +196,14 @@ const response = await fetchAdBanner({
|
|
|
196
196
|
});
|
|
197
197
|
|
|
198
198
|
// Track events (automatically skipped in TheAd Vantage dev mode)
|
|
199
|
-
trackImpression
|
|
200
|
-
|
|
199
|
+
// Note: trackImpression and trackClick now require apiKey parameter
|
|
200
|
+
// They automatically include client fingerprinting data for fraud detection
|
|
201
|
+
trackImpression(adId, apiKey);
|
|
202
|
+
trackClick(adId, apiKey);
|
|
203
|
+
|
|
204
|
+
// You can also collect fingerprint data manually if needed
|
|
205
|
+
const fingerprint = collectFingerprint();
|
|
206
|
+
console.log('Client fingerprint:', fingerprint);
|
|
201
207
|
```
|
|
202
208
|
|
|
203
209
|
---
|
|
@@ -335,8 +341,9 @@ The TheAd Vantage integration uses a smart mode detection system to support diff
|
|
|
335
341
|
### Utilities
|
|
336
342
|
- **`src/lib/ads.ts`**: Utility functions for fetching and tracking ads
|
|
337
343
|
- `fetchAdBanner(params)`: Fetches ads with full parameter support
|
|
338
|
-
- `trackImpression(adId)`: Tracks ad impressions (skipped in dev mode)
|
|
339
|
-
- `trackClick(adId)`: Tracks ad clicks (skipped in dev mode)
|
|
344
|
+
- `trackImpression(adId, apiKey, apiUrl?)`: Tracks ad impressions with fingerprinting (skipped in dev mode)
|
|
345
|
+
- `trackClick(adId, apiKey, apiUrl?)`: Tracks ad clicks with fingerprinting (skipped in dev mode)
|
|
346
|
+
- `collectFingerprint()`: Collects client fingerprinting data for fraud detection
|
|
340
347
|
|
|
341
348
|
## Implementation Instructions for AI Agents
|
|
342
349
|
|
package/package.json
CHANGED
|
@@ -58,7 +58,7 @@ export function AdBanner({
|
|
|
58
58
|
setDevMode(response.dev_mode || false);
|
|
59
59
|
|
|
60
60
|
// Track impression (will be skipped in dev mode)
|
|
61
|
-
trackImpression(response.ad.id);
|
|
61
|
+
trackImpression(response.ad.id, apiKey, apiUrl);
|
|
62
62
|
} else {
|
|
63
63
|
// Create a more detailed error message
|
|
64
64
|
const errorDetails = [];
|
|
@@ -106,7 +106,7 @@ export function AdBanner({
|
|
|
106
106
|
|
|
107
107
|
const handleClick = () => {
|
|
108
108
|
if (ad) {
|
|
109
|
-
trackClick(ad.id);
|
|
109
|
+
trackClick(ad.id, apiKey, apiUrl);
|
|
110
110
|
// The link will handle navigation
|
|
111
111
|
}
|
|
112
112
|
};
|
|
@@ -42,7 +42,14 @@ export default function AdDisplay({ position, className = '' }: AdDisplayProps)
|
|
|
42
42
|
setDevMode(response.dev_mode || false);
|
|
43
43
|
|
|
44
44
|
// Track impression (will be skipped in dev mode)
|
|
45
|
-
|
|
45
|
+
// Note: AdDisplay doesn't have apiKey, so tracking will fail gracefully
|
|
46
|
+
// This component may need to be updated to accept apiKey prop if tracking is needed
|
|
47
|
+
if (response.ad.id) {
|
|
48
|
+
// Silently fail if no API key available
|
|
49
|
+
trackImpression(response.ad.id, '').catch(() => {
|
|
50
|
+
// Tracking failed - this is expected without API key
|
|
51
|
+
});
|
|
52
|
+
}
|
|
46
53
|
} else {
|
|
47
54
|
setError('No ad available');
|
|
48
55
|
}
|
|
@@ -59,7 +66,11 @@ export default function AdDisplay({ position, className = '' }: AdDisplayProps)
|
|
|
59
66
|
|
|
60
67
|
const handleClick = () => {
|
|
61
68
|
if (ad) {
|
|
62
|
-
|
|
69
|
+
// Note: AdDisplay doesn't have apiKey, so tracking will fail gracefully
|
|
70
|
+
// This component may need to be updated to accept apiKey prop if tracking is needed
|
|
71
|
+
trackClick(ad.id, '').catch(() => {
|
|
72
|
+
// Tracking failed - this is expected without API key
|
|
73
|
+
});
|
|
63
74
|
// The link will handle navigation
|
|
64
75
|
}
|
|
65
76
|
};
|
package/src/index.ts
CHANGED
|
@@ -14,11 +14,13 @@ export {
|
|
|
14
14
|
fetchAdBanner,
|
|
15
15
|
trackImpression,
|
|
16
16
|
trackClick,
|
|
17
|
+
collectFingerprint,
|
|
17
18
|
type Ad,
|
|
18
19
|
type AdData,
|
|
19
20
|
type AdsResponse,
|
|
20
21
|
type AdBannerResponse,
|
|
21
22
|
type FetchAdBannerParams,
|
|
23
|
+
type Fingerprint,
|
|
22
24
|
} from './lib/ads';
|
|
23
25
|
|
|
24
26
|
// Export configuration utilities
|
package/src/lib/ads.ts
CHANGED
|
@@ -360,14 +360,76 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Collect client fingerprinting data for fraud detection
|
|
365
|
+
* This data is used by the server to detect duplicate impressions, bot traffic, and fraudulent clicks
|
|
366
|
+
*/
|
|
367
|
+
export interface Fingerprint {
|
|
368
|
+
screenWidth: number | null;
|
|
369
|
+
screenHeight: number | null;
|
|
370
|
+
timezone: string | null;
|
|
371
|
+
language: string | null;
|
|
372
|
+
colorDepth: number | null;
|
|
373
|
+
platform: string | null;
|
|
374
|
+
cookiesEnabled: boolean | null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function collectFingerprint(): Fingerprint {
|
|
378
|
+
// Only collect fingerprint in browser context
|
|
379
|
+
if (typeof window === 'undefined') {
|
|
380
|
+
return {
|
|
381
|
+
screenWidth: null,
|
|
382
|
+
screenHeight: null,
|
|
383
|
+
timezone: null,
|
|
384
|
+
language: null,
|
|
385
|
+
colorDepth: null,
|
|
386
|
+
platform: null,
|
|
387
|
+
cookiesEnabled: null,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
return {
|
|
393
|
+
screenWidth: window.screen?.width ?? null,
|
|
394
|
+
screenHeight: window.screen?.height ?? null,
|
|
395
|
+
timezone: Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.timeZone ?? null,
|
|
396
|
+
language: navigator?.language ?? null,
|
|
397
|
+
colorDepth: window.screen?.colorDepth ?? null,
|
|
398
|
+
platform: navigator?.platform ?? null,
|
|
399
|
+
cookiesEnabled: navigator?.cookieEnabled ?? null,
|
|
400
|
+
};
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.warn('[AdBanner] Error collecting fingerprint:', error);
|
|
403
|
+
return {
|
|
404
|
+
screenWidth: null,
|
|
405
|
+
screenHeight: null,
|
|
406
|
+
timezone: null,
|
|
407
|
+
language: null,
|
|
408
|
+
colorDepth: null,
|
|
409
|
+
platform: null,
|
|
410
|
+
cookiesEnabled: null,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
363
415
|
/**
|
|
364
416
|
* Track an ad impression (when ad is viewed)
|
|
365
417
|
* Sends tracking directly to TheAd Vantage API (or skips in dev mode)
|
|
418
|
+
* Includes client fingerprinting data for fraud detection
|
|
419
|
+
*
|
|
420
|
+
* @param adId - The ID of the ad being tracked
|
|
421
|
+
* @param apiKey - The API key for the platform (required for CORS validation and platform identification)
|
|
422
|
+
* @param apiUrl - Optional API base URL override
|
|
366
423
|
*/
|
|
367
|
-
export async function trackImpression(adId: string): Promise<void> {
|
|
424
|
+
export async function trackImpression(adId: string, apiKey: string, apiUrl?: string): Promise<void> {
|
|
368
425
|
try {
|
|
426
|
+
if (!apiKey) {
|
|
427
|
+
console.warn('[AdBanner] Cannot track impression: API key is required');
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
369
431
|
// Get the API base URL to determine where to send tracking
|
|
370
|
-
const apiBaseUrl = getApiBaseUrl();
|
|
432
|
+
const apiBaseUrl = getApiBaseUrl(apiUrl);
|
|
371
433
|
|
|
372
434
|
// Build tracking URL - remove /api/ads if present, then append /api/ads/track
|
|
373
435
|
let trackingUrl = apiBaseUrl.trim();
|
|
@@ -387,6 +449,10 @@ export async function trackImpression(adId: string): Promise<void> {
|
|
|
387
449
|
trackingUrl = trackingUrl.replace('thead-vantage.com', 'www.thead-vantage.com');
|
|
388
450
|
}
|
|
389
451
|
|
|
452
|
+
// Add API key to query string (recommended for CORS preflight validation)
|
|
453
|
+
const url = new URL(trackingUrl);
|
|
454
|
+
url.searchParams.set('api_key', apiKey);
|
|
455
|
+
|
|
390
456
|
// Check if we should skip tracking (dev mode)
|
|
391
457
|
const useDevFlags = shouldUseDevFlags();
|
|
392
458
|
if (useDevFlags) {
|
|
@@ -394,7 +460,7 @@ export async function trackImpression(adId: string): Promise<void> {
|
|
|
394
460
|
return;
|
|
395
461
|
}
|
|
396
462
|
|
|
397
|
-
const response = await fetch(
|
|
463
|
+
const response = await fetch(url.toString(), {
|
|
398
464
|
method: 'POST',
|
|
399
465
|
mode: 'cors', // Explicitly enable CORS
|
|
400
466
|
credentials: 'omit', // Don't send cookies
|
|
@@ -405,17 +471,23 @@ export async function trackImpression(adId: string): Promise<void> {
|
|
|
405
471
|
body: JSON.stringify({
|
|
406
472
|
action: 'impression',
|
|
407
473
|
adId,
|
|
474
|
+
fingerprint: collectFingerprint(), // Include fingerprint data for fraud detection
|
|
408
475
|
}),
|
|
409
476
|
});
|
|
410
477
|
|
|
411
478
|
if (!response.ok) {
|
|
412
|
-
|
|
479
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
480
|
+
console.warn(`[AdBanner] Tracking impression failed: ${response.status} ${response.statusText}`, {
|
|
481
|
+
errorText: errorText.substring(0, 200),
|
|
482
|
+
});
|
|
413
483
|
return;
|
|
414
484
|
}
|
|
415
485
|
|
|
416
486
|
const data = await response.json();
|
|
417
487
|
if (data.dev_mode) {
|
|
418
488
|
console.log(`[DEV] Impression tracking skipped for ad: ${adId}`);
|
|
489
|
+
} else {
|
|
490
|
+
console.debug(`[AdBanner] Impression tracked successfully for ad: ${adId}`);
|
|
419
491
|
}
|
|
420
492
|
} catch (error) {
|
|
421
493
|
// Handle CORS errors gracefully (tracking failures shouldn't break the app)
|
|
@@ -433,11 +505,20 @@ export async function trackImpression(adId: string): Promise<void> {
|
|
|
433
505
|
/**
|
|
434
506
|
* Track an ad click
|
|
435
507
|
* Sends tracking directly to TheAd Vantage API (or skips in dev mode)
|
|
508
|
+
*
|
|
509
|
+
* @param adId - The ID of the ad being tracked
|
|
510
|
+
* @param apiKey - The API key for the platform (required for CORS validation and platform identification)
|
|
511
|
+
* @param apiUrl - Optional API base URL override
|
|
436
512
|
*/
|
|
437
|
-
export async function trackClick(adId: string): Promise<void> {
|
|
513
|
+
export async function trackClick(adId: string, apiKey: string, apiUrl?: string): Promise<void> {
|
|
438
514
|
try {
|
|
515
|
+
if (!apiKey) {
|
|
516
|
+
console.warn('[AdBanner] Cannot track click: API key is required');
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
439
520
|
// Get the API base URL to determine where to send tracking
|
|
440
|
-
const apiBaseUrl = getApiBaseUrl();
|
|
521
|
+
const apiBaseUrl = getApiBaseUrl(apiUrl);
|
|
441
522
|
|
|
442
523
|
// Build tracking URL - remove /api/ads if present, then append /api/ads/track
|
|
443
524
|
let trackingUrl = apiBaseUrl.trim();
|
|
@@ -457,6 +538,10 @@ export async function trackClick(adId: string): Promise<void> {
|
|
|
457
538
|
trackingUrl = trackingUrl.replace('thead-vantage.com', 'www.thead-vantage.com');
|
|
458
539
|
}
|
|
459
540
|
|
|
541
|
+
// Add API key to query string (recommended for CORS preflight validation)
|
|
542
|
+
const url = new URL(trackingUrl);
|
|
543
|
+
url.searchParams.set('api_key', apiKey);
|
|
544
|
+
|
|
460
545
|
// Check if we should skip tracking (dev mode)
|
|
461
546
|
const useDevFlags = shouldUseDevFlags();
|
|
462
547
|
if (useDevFlags) {
|
|
@@ -464,7 +549,7 @@ export async function trackClick(adId: string): Promise<void> {
|
|
|
464
549
|
return;
|
|
465
550
|
}
|
|
466
551
|
|
|
467
|
-
const response = await fetch(
|
|
552
|
+
const response = await fetch(url.toString(), {
|
|
468
553
|
method: 'POST',
|
|
469
554
|
mode: 'cors', // Explicitly enable CORS
|
|
470
555
|
credentials: 'omit', // Don't send cookies
|
|
@@ -475,17 +560,23 @@ export async function trackClick(adId: string): Promise<void> {
|
|
|
475
560
|
body: JSON.stringify({
|
|
476
561
|
action: 'click',
|
|
477
562
|
adId,
|
|
563
|
+
fingerprint: collectFingerprint(), // Include fingerprint data for fraud detection
|
|
478
564
|
}),
|
|
479
565
|
});
|
|
480
566
|
|
|
481
567
|
if (!response.ok) {
|
|
482
|
-
|
|
568
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
569
|
+
console.warn(`[AdBanner] Tracking click failed: ${response.status} ${response.statusText}`, {
|
|
570
|
+
errorText: errorText.substring(0, 200),
|
|
571
|
+
});
|
|
483
572
|
return;
|
|
484
573
|
}
|
|
485
574
|
|
|
486
575
|
const data = await response.json();
|
|
487
576
|
if (data.dev_mode) {
|
|
488
577
|
console.log(`[DEV] Click tracking skipped for ad: ${adId}`);
|
|
578
|
+
} else {
|
|
579
|
+
console.debug(`[AdBanner] Click tracked successfully for ad: ${adId}`);
|
|
489
580
|
}
|
|
490
581
|
} catch (error) {
|
|
491
582
|
// Handle CORS errors gracefully (tracking failures shouldn't break the app)
|