@thead-vantage/react 2.18.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 +132 -8
- package/package.json +1 -1
- package/src/components/AdBanner.tsx +72 -5
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=
|
|
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
|
-
|
|
202
|
-
|
|
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:
|
|
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
|
|
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
|
'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
|
|
|
@@ -14,6 +14,7 @@ export interface AdBannerProps {
|
|
|
14
14
|
className?: string;
|
|
15
15
|
clickMetadata?: Record<string, unknown>; // Optional metadata to send with click events
|
|
16
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
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function AdBanner({
|
|
@@ -26,11 +27,14 @@ export function AdBanner({
|
|
|
26
27
|
className = '',
|
|
27
28
|
clickMetadata,
|
|
28
29
|
impressionMetadata,
|
|
30
|
+
showImpressionFinished = false,
|
|
29
31
|
}: AdBannerProps) {
|
|
30
32
|
const [ad, setAd] = useState<Ad | null>(null);
|
|
31
33
|
const [loading, setLoading] = useState(true);
|
|
32
34
|
const [error, setError] = useState<string | null>(null);
|
|
33
35
|
const [devMode, setDevMode] = useState(false);
|
|
36
|
+
const [impressionStatus, setImpressionStatus] = useState<'pending' | 'counting' | 'completed'>('pending');
|
|
37
|
+
const impressionTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
34
38
|
|
|
35
39
|
useEffect(() => {
|
|
36
40
|
const loadAd = async () => {
|
|
@@ -66,8 +70,24 @@ export function AdBanner({
|
|
|
66
70
|
setAd(response.ad);
|
|
67
71
|
setDevMode(response.dev_mode || false);
|
|
68
72
|
|
|
69
|
-
//
|
|
70
|
-
|
|
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
|
+
}
|
|
71
91
|
} else {
|
|
72
92
|
// Create a more detailed error message
|
|
73
93
|
const errorDetails = [];
|
|
@@ -111,7 +131,15 @@ export function AdBanner({
|
|
|
111
131
|
};
|
|
112
132
|
|
|
113
133
|
loadAd();
|
|
114
|
-
|
|
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]);
|
|
115
143
|
|
|
116
144
|
const handleClick = () => {
|
|
117
145
|
if (ad) {
|
|
@@ -137,7 +165,7 @@ export function AdBanner({
|
|
|
137
165
|
}
|
|
138
166
|
|
|
139
167
|
return (
|
|
140
|
-
<div className={className}>
|
|
168
|
+
<div className={`relative ${className}`}>
|
|
141
169
|
<a
|
|
142
170
|
href={ad.targetUrl}
|
|
143
171
|
onClick={handleClick}
|
|
@@ -160,6 +188,45 @@ export function AdBanner({
|
|
|
160
188
|
</div>
|
|
161
189
|
)}
|
|
162
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
|
+
)}
|
|
163
230
|
{devMode && (
|
|
164
231
|
<p className="text-xs text-gray-500 mt-1">[DEV] No tracking active</p>
|
|
165
232
|
)}
|