@thead-vantage/react 2.17.0 → 2.19.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 CHANGED
@@ -151,12 +151,24 @@ The main component for displaying ads from TheAd Vantage.
151
151
  | `userId` | `string \| null` | No | `null` | User ID for ad targeting |
152
152
  | `userSegment` | `string \| null` | No | `null` | User segment for ad targeting |
153
153
  | `className` | `string` | No | `''` | Additional CSS classes |
154
+ | `clickMetadata` | `Record<string, unknown>` | No | - | Optional metadata to send with click tracking events |
155
+ | `impressionMetadata` | `Record<string, unknown>` | No | - | Optional metadata to send with impression tracking events |
156
+ | `showImpressionFinished` | `boolean` | No | `false` | Show spinning checkmark during impression timer (2s), then green checkmark when complete |
154
157
 
155
158
  **Example**:
156
159
  ```tsx
157
160
  import { AdBanner } from '@/components/AdBanner';
158
161
 
159
162
  export default function ArticlePage() {
163
+ const userData = {
164
+ id: 'user-123',
165
+ email: 'user@example.com',
166
+ school: 'University of Example',
167
+ grade: 'Senior',
168
+ major: 'Computer Science',
169
+ role: 'student',
170
+ };
171
+
160
172
  return (
161
173
  <div>
162
174
  <article>
@@ -169,9 +181,26 @@ export default function ArticlePage() {
169
181
  platformId="10"
170
182
  apiKey="abc123xyz"
171
183
  size="medium-rectangle"
172
- userId="user-123"
184
+ userId={userData.id}
173
185
  userSegment="premium"
174
186
  className="my-4"
187
+ clickMetadata={{
188
+ userId: userData.id,
189
+ email: userData.email,
190
+ school: userData.school,
191
+ grade: userData.grade,
192
+ major: userData.major,
193
+ role: userData.role,
194
+ }}
195
+ impressionMetadata={{
196
+ userId: userData.id,
197
+ email: userData.email,
198
+ school: userData.school,
199
+ grade: userData.grade,
200
+ major: userData.major,
201
+ role: userData.role,
202
+ }}
203
+ showImpressionFinished={true}
175
204
  />
176
205
  </aside>
177
206
  </div>
@@ -179,6 +208,60 @@ export default function ArticlePage() {
179
208
  }
180
209
  ```
181
210
 
211
+ ### Metadata Tracking
212
+
213
+ The `AdBanner` component supports optional metadata that is sent with click and impression tracking events. This allows you to pass additional context about the user or session to TheAd Vantage for analytics purposes.
214
+
215
+ **Metadata Props**:
216
+ - `clickMetadata`: Optional object with key-value pairs sent with click events
217
+ - `impressionMetadata`: Optional object with key-value pairs sent with impression events
218
+
219
+ **Example with Metadata**:
220
+ ```tsx
221
+ <AdBanner
222
+ platformId="10"
223
+ apiKey="abc123xyz"
224
+ size="medium-rectangle"
225
+ clickMetadata={{
226
+ userId: userData?.id,
227
+ email: userData?.email,
228
+ school: userData?.school,
229
+ grade: userData?.grade,
230
+ major: userData?.major,
231
+ role: userData?.role,
232
+ }}
233
+ impressionMetadata={{
234
+ userId: userData?.id,
235
+ email: userData?.email,
236
+ school: userData?.school,
237
+ grade: userData?.grade,
238
+ major: userData?.major,
239
+ role: userData?.role,
240
+ }}
241
+ />
242
+ ```
243
+
244
+ **Note**: The metadata is sent to the `/api/ads/track` endpoint on thead-vantage.com. Ensure the API has been updated to support receiving metadata (see `other/thead-vantage-api-metadata-prompt.md` for implementation details).
245
+
246
+ ### Impression Finished Indicator
247
+
248
+ The `showImpressionFinished` prop enables a visual indicator that shows when an impression has been recorded:
249
+
250
+ - **Spinning Checkmark**: A gray spinning checkmark appears in the top-right corner of the ad during the 2-second impression timer
251
+ - **Green Checkmark**: After 2 seconds, the checkmark turns green to indicate the impression has been recorded
252
+
253
+ **Example**:
254
+ ```tsx
255
+ <AdBanner
256
+ platformId="10"
257
+ apiKey="abc123xyz"
258
+ size="banner"
259
+ showImpressionFinished={true}
260
+ />
261
+ ```
262
+
263
+ This feature is useful for debugging and providing visual feedback that impressions are being tracked correctly.
264
+
182
265
  ### Utility Functions
183
266
 
184
267
  You can also use the utility functions directly:
@@ -198,8 +281,16 @@ const response = await fetchAdBanner({
198
281
  // Track events (automatically skipped in TheAd Vantage dev mode)
199
282
  // Note: trackImpression and trackClick now require apiKey parameter
200
283
  // They automatically include client fingerprinting data for fraud detection
201
- trackImpression(adId, apiKey);
202
- trackClick(adId, apiKey);
284
+ // Optional metadata can be passed as the last parameter
285
+ trackImpression(adId, apiKey, apiUrl, {
286
+ userId: 'user-123',
287
+ email: 'user@example.com',
288
+ school: 'University of Example',
289
+ });
290
+ trackClick(adId, apiKey, apiUrl, {
291
+ userId: 'user-123',
292
+ email: 'user@example.com',
293
+ });
203
294
 
204
295
  // You can also collect fingerprint data manually if needed
205
296
  const fingerprint = collectFingerprint();
@@ -335,14 +426,16 @@ The TheAd Vantage integration uses a smart mode detection system to support diff
335
426
 
336
427
  ### Components
337
428
  - **`src/components/AdBanner.tsx`**: Main React component for displaying ads
338
- - Props: `platformId`, `apiKey`, `size`, `apiUrl?`, `userId?`, `userSegment?`, `className?`
429
+ - Props: `platformId`, `apiKey`, `size`, `apiUrl?`, `userId?`, `userSegment?`, `className?`, `clickMetadata?`, `impressionMetadata?`, `showImpressionFinished?`
339
430
  - Automatically handles loading, error states, and dev mode indicators
431
+ - Supports optional metadata for click and impression tracking
432
+ - Optional impression finished indicator (spinning checkmark → green checkmark)
340
433
 
341
434
  ### Utilities
342
435
  - **`src/lib/ads.ts`**: Utility functions for fetching and tracking ads
343
436
  - `fetchAdBanner(params)`: Fetches ads with full parameter support
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)
437
+ - `trackImpression(adId, apiKey, apiUrl?, metadata?)`: Tracks ad impressions with fingerprinting and optional metadata (skipped in dev mode)
438
+ - `trackClick(adId, apiKey, apiUrl?, metadata?)`: Tracks ad clicks with fingerprinting and optional metadata (skipped in dev mode)
346
439
  - `collectFingerprint()`: Collects client fingerprinting data for fraud detection
347
440
 
348
441
  ## Implementation Instructions for AI Agents
@@ -415,7 +508,38 @@ The system uses this priority order to determine which API URL to use:
415
508
  />
416
509
  ```
417
510
 
418
- **Pattern 3: Custom API URL Override**
511
+ **Pattern 3: Ad with Metadata Tracking**
512
+ ```tsx
513
+ <AdBanner
514
+ platformId="10"
515
+ apiKey="key"
516
+ size="medium-rectangle"
517
+ clickMetadata={{
518
+ userId: userData?.id,
519
+ email: userData?.email,
520
+ school: userData?.school,
521
+ grade: userData?.grade,
522
+ }}
523
+ impressionMetadata={{
524
+ userId: userData?.id,
525
+ email: userData?.email,
526
+ school: userData?.school,
527
+ grade: userData?.grade,
528
+ }}
529
+ />
530
+ ```
531
+
532
+ **Pattern 4: Ad with Impression Finished Indicator**
533
+ ```tsx
534
+ <AdBanner
535
+ platformId="10"
536
+ apiKey="key"
537
+ size="banner"
538
+ showImpressionFinished={true}
539
+ />
540
+ ```
541
+
542
+ **Pattern 5: Custom API URL Override**
419
543
  ```tsx
420
544
  <AdBanner
421
545
  platformId="10"
@@ -425,7 +549,7 @@ The system uses this priority order to determine which API URL to use:
425
549
  />
426
550
  ```
427
551
 
428
- **Pattern 4: Programmatic Ad Fetching**
552
+ **Pattern 6: Programmatic Ad Fetching**
429
553
  ```tsx
430
554
  const response = await fetchAdBanner({
431
555
  platformId: '10',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thead-vantage/react",
3
- "version": "2.17.0",
3
+ "version": "2.19.0",
4
4
  "description": "React components and utilities for TheAd Vantage ad platform integration",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState } from 'react';
3
+ import { useEffect, useState, useRef } from 'react';
4
4
  import Image from 'next/image';
5
5
  import { fetchAdBanner, trackImpression, trackClick, type Ad } from '../lib/ads';
6
6
 
@@ -12,6 +12,9 @@ export interface AdBannerProps {
12
12
  userId?: string | null;
13
13
  userSegment?: string | null;
14
14
  className?: string;
15
+ clickMetadata?: Record<string, unknown>; // Optional metadata to send with click events
16
+ impressionMetadata?: Record<string, unknown>; // Optional metadata to send with impression events
17
+ showImpressionFinished?: boolean; // Show spinning checkmark during impression timer, then green check when complete
15
18
  }
16
19
 
17
20
  export function AdBanner({
@@ -22,11 +25,16 @@ export function AdBanner({
22
25
  userId = null,
23
26
  userSegment = null,
24
27
  className = '',
28
+ clickMetadata,
29
+ impressionMetadata,
30
+ showImpressionFinished = false,
25
31
  }: AdBannerProps) {
26
32
  const [ad, setAd] = useState<Ad | null>(null);
27
33
  const [loading, setLoading] = useState(true);
28
34
  const [error, setError] = useState<string | null>(null);
29
35
  const [devMode, setDevMode] = useState(false);
36
+ const [impressionStatus, setImpressionStatus] = useState<'pending' | 'counting' | 'completed'>('pending');
37
+ const impressionTimerRef = useRef<NodeJS.Timeout | null>(null);
30
38
 
31
39
  useEffect(() => {
32
40
  const loadAd = async () => {
@@ -62,8 +70,24 @@ export function AdBanner({
62
70
  setAd(response.ad);
63
71
  setDevMode(response.dev_mode || false);
64
72
 
65
- // Track impression (will be skipped in dev mode)
66
- trackImpression(response.ad.id, apiKey, apiUrl);
73
+ // Start impression timer if showImpressionFinished is enabled
74
+ if (showImpressionFinished) {
75
+ setImpressionStatus('counting');
76
+ // Standard impression timer: 2 seconds of view time
77
+ impressionTimerRef.current = setTimeout(() => {
78
+ setImpressionStatus('completed');
79
+ impressionTimerRef.current = null;
80
+ }, 2000);
81
+
82
+ // Track impression (will be skipped in dev mode)
83
+ // Note: We don't wait for the API call to complete - the timer determines when to show green check
84
+ trackImpression(response.ad.id, apiKey, apiUrl, impressionMetadata).catch(() => {
85
+ // Silently handle tracking errors - timer will still complete
86
+ });
87
+ } else {
88
+ // Track impression without timer UI
89
+ trackImpression(response.ad.id, apiKey, apiUrl, impressionMetadata);
90
+ }
67
91
  } else {
68
92
  // Create a more detailed error message
69
93
  const errorDetails = [];
@@ -107,11 +131,19 @@ export function AdBanner({
107
131
  };
108
132
 
109
133
  loadAd();
110
- }, [platformId, apiKey, size, apiUrl, userId, userSegment]);
134
+
135
+ // Cleanup timer on unmount or when dependencies change
136
+ return () => {
137
+ if (impressionTimerRef.current) {
138
+ clearTimeout(impressionTimerRef.current);
139
+ impressionTimerRef.current = null;
140
+ }
141
+ };
142
+ }, [platformId, apiKey, size, apiUrl, userId, userSegment, showImpressionFinished, impressionMetadata]);
111
143
 
112
144
  const handleClick = () => {
113
145
  if (ad) {
114
- trackClick(ad.id, apiKey, apiUrl);
146
+ trackClick(ad.id, apiKey, apiUrl, clickMetadata);
115
147
  // The link will handle navigation
116
148
  }
117
149
  };
@@ -133,7 +165,7 @@ export function AdBanner({
133
165
  }
134
166
 
135
167
  return (
136
- <div className={className}>
168
+ <div className={`relative ${className}`}>
137
169
  <a
138
170
  href={ad.targetUrl}
139
171
  onClick={handleClick}
@@ -156,6 +188,45 @@ export function AdBanner({
156
188
  </div>
157
189
  )}
158
190
  </a>
191
+ {showImpressionFinished && impressionStatus !== 'pending' && (
192
+ <div className="absolute top-2 right-2">
193
+ {impressionStatus === 'counting' ? (
194
+ <svg
195
+ className="w-5 h-5 text-gray-400 animate-spin"
196
+ xmlns="http://www.w3.org/2000/svg"
197
+ fill="none"
198
+ viewBox="0 0 24 24"
199
+ >
200
+ <circle
201
+ className="opacity-25"
202
+ cx="12"
203
+ cy="12"
204
+ r="10"
205
+ stroke="currentColor"
206
+ strokeWidth="4"
207
+ />
208
+ <path
209
+ className="opacity-75"
210
+ fill="currentColor"
211
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
212
+ />
213
+ </svg>
214
+ ) : impressionStatus === 'completed' ? (
215
+ <svg
216
+ className="w-5 h-5 text-green-500"
217
+ xmlns="http://www.w3.org/2000/svg"
218
+ viewBox="0 0 20 20"
219
+ fill="currentColor"
220
+ >
221
+ <path
222
+ fillRule="evenodd"
223
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
224
+ clipRule="evenodd"
225
+ />
226
+ </svg>
227
+ ) : null}
228
+ </div>
229
+ )}
159
230
  {devMode && (
160
231
  <p className="text-xs text-gray-500 mt-1">[DEV] No tracking active</p>
161
232
  )}
package/src/lib/ads.ts CHANGED
@@ -430,8 +430,9 @@ export function collectFingerprint(): Fingerprint {
430
430
  * @param adId - The ID of the ad being tracked
431
431
  * @param apiKey - The API key for the platform (required for CORS validation and platform identification)
432
432
  * @param apiUrl - Optional API base URL override
433
+ * @param metadata - Optional metadata object to send with the impression event
433
434
  */
434
- export async function trackImpression(adId: string, apiKey: string, apiUrl?: string): Promise<void> {
435
+ export async function trackImpression(adId: string, apiKey: string, apiUrl?: string, metadata?: Record<string, unknown>): Promise<void> {
435
436
  try {
436
437
  if (!apiKey) {
437
438
  console.warn('[AdBanner] Cannot track impression: API key is required');
@@ -482,6 +483,7 @@ export async function trackImpression(adId: string, apiKey: string, apiUrl?: str
482
483
  action: 'impression',
483
484
  adId,
484
485
  fingerprint: collectFingerprint(), // Include fingerprint data for fraud detection
486
+ ...(metadata && { metadata }), // Include metadata if provided
485
487
  }),
486
488
  });
487
489
 
@@ -519,8 +521,9 @@ export async function trackImpression(adId: string, apiKey: string, apiUrl?: str
519
521
  * @param adId - The ID of the ad being tracked
520
522
  * @param apiKey - The API key for the platform (required for CORS validation and platform identification)
521
523
  * @param apiUrl - Optional API base URL override
524
+ * @param metadata - Optional metadata object to send with the click event
522
525
  */
523
- export async function trackClick(adId: string, apiKey: string, apiUrl?: string): Promise<void> {
526
+ export async function trackClick(adId: string, apiKey: string, apiUrl?: string, metadata?: Record<string, unknown>): Promise<void> {
524
527
  try {
525
528
  if (!apiKey) {
526
529
  console.warn('[AdBanner] Cannot track click: API key is required');
@@ -571,6 +574,7 @@ export async function trackClick(adId: string, apiKey: string, apiUrl?: string):
571
574
  action: 'click',
572
575
  adId,
573
576
  fingerprint: collectFingerprint(), // Include fingerprint data for fraud detection
577
+ ...(metadata && { metadata }), // Include metadata if provided
574
578
  }),
575
579
  });
576
580