@mixpeek/prebid 1.0.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/CHANGELOG.md +153 -0
- package/ENDPOINTS.md +308 -0
- package/LICENSE +68 -0
- package/QUICKSTART.md +234 -0
- package/README.md +439 -0
- package/TESTING.md +341 -0
- package/dist/mixpeekContextAdapter.js +3 -0
- package/dist/mixpeekContextAdapter.js.LICENSE.txt +1 -0
- package/dist/mixpeekContextAdapter.js.map +1 -0
- package/docs/MIGRATION_V2.md +519 -0
- package/docs/api-reference.md +455 -0
- package/docs/health-check.md +348 -0
- package/docs/integration-guide.md +577 -0
- package/examples/publisher-demo/README.md +65 -0
- package/examples/publisher-demo/index.html +331 -0
- package/examples/publisher-demo/package.json +11 -0
- package/package.json +82 -0
- package/src/api/mixpeekClient.js +303 -0
- package/src/cache/cacheManager.js +245 -0
- package/src/config/constants.js +125 -0
- package/src/extractors/imageExtractor.js +142 -0
- package/src/extractors/pageExtractor.js +196 -0
- package/src/extractors/videoExtractor.js +228 -0
- package/src/modules/mixpeekContextAdapter.js +833 -0
- package/src/modules/mixpeekRtdProvider.js +305 -0
- package/src/prebid/prebidIntegration.js +117 -0
- package/src/utils/helpers.js +261 -0
- package/src/utils/iabMapping.js +367 -0
- package/src/utils/logger.js +64 -0
- package/src/utils/previousAdTracker.js +95 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IAB Content Taxonomy Mapping
|
|
3
|
+
* @module utils/iabMapping
|
|
4
|
+
*
|
|
5
|
+
* Maps Mixpeek taxonomy to IAB Content Taxonomy v3.0 codes
|
|
6
|
+
* Reference: https://iabtechlab.com/standards/content-taxonomy/
|
|
7
|
+
*
|
|
8
|
+
* ✅ VERIFIED: Mixpeek Response Format (from OpenAPI spec)
|
|
9
|
+
*
|
|
10
|
+
* Mixpeek returns TaxonomyAssignment objects in enrichments.taxonomies:
|
|
11
|
+
* ```javascript
|
|
12
|
+
* {
|
|
13
|
+
* "taxonomy_id": "tax_products",
|
|
14
|
+
* "node_id": "node_electronics_phones", // ← Custom Mixpeek node ID
|
|
15
|
+
* "path": ["products", "electronics", "phones"],
|
|
16
|
+
* "label": "Mobile Phones", // Human-readable label
|
|
17
|
+
* "score": 0.87
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* This means we're in **Scenario B**: Mixpeek uses custom node_ids like
|
|
22
|
+
* "node_electronics_phones" that need to be mapped to IAB codes like "IAB19-20".
|
|
23
|
+
*
|
|
24
|
+
* Three mapping strategies (in order of priority):
|
|
25
|
+
*
|
|
26
|
+
* 1. **Check if already IAB** (safety check)
|
|
27
|
+
* - If node_id or label contains IAB code pattern, use it directly
|
|
28
|
+
*
|
|
29
|
+
* 2. **Map by node_id** (primary method) ⭐
|
|
30
|
+
* - Map Mixpeek node_id → IAB code using MIXPEEK_NODE_TO_IAB
|
|
31
|
+
* - Most reliable when properly configured
|
|
32
|
+
* - Requires discovering actual node_ids from your taxonomy
|
|
33
|
+
*
|
|
34
|
+
* 3. **Map by label** (fallback)
|
|
35
|
+
* - Map human-readable labels to IAB codes
|
|
36
|
+
* - Less precise but provides coverage
|
|
37
|
+
*
|
|
38
|
+
* To populate the mapping with YOUR taxonomy's node_ids:
|
|
39
|
+
* ```bash
|
|
40
|
+
* # Run the verification script to discover node_ids
|
|
41
|
+
* MIXPEEK_API_KEY=your_key COLLECTION_ID=your_collection \
|
|
42
|
+
* node scripts/verify-mixpeek-taxonomy.js
|
|
43
|
+
*
|
|
44
|
+
* # This will show you the actual node_id values like:
|
|
45
|
+
* # node_id: "node_electronics_phones" → Map to: IAB19-20
|
|
46
|
+
* # node_id: "node_sports_football" → Map to: IAB17-3
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* IAB Taxonomy version identifier
|
|
52
|
+
* IAB Tech Lab Content Taxonomy v3.0 = 6
|
|
53
|
+
*/
|
|
54
|
+
export const IAB_TAXONOMY_VERSION = 6
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Map Mixpeek node_ids to IAB codes
|
|
58
|
+
*
|
|
59
|
+
* ✅ CONFIRMED: Mixpeek uses custom node_ids (from OpenAPI spec)
|
|
60
|
+
* Example from spec: "node_electronics_phones" → should map to "IAB19-20"
|
|
61
|
+
*
|
|
62
|
+
* ⚠️ TODO: Populate with YOUR taxonomy's actual node_ids
|
|
63
|
+
* The placeholders below use common patterns, but you need to run the
|
|
64
|
+
* verification script to get the exact node_ids from YOUR Mixpeek taxonomy.
|
|
65
|
+
*
|
|
66
|
+
* Format: 'mixpeek_node_id': 'IAB_CODE'
|
|
67
|
+
*
|
|
68
|
+
* To discover YOUR node_ids:
|
|
69
|
+
* 1. Run: node scripts/verify-mixpeek-taxonomy.js
|
|
70
|
+
* 2. Note the node_id values returned
|
|
71
|
+
* 3. Map each to the appropriate IAB code
|
|
72
|
+
*
|
|
73
|
+
* Example mapping (update with your actual node_ids):
|
|
74
|
+
* 'node_tech_ai': 'IAB19-11', // Tech > AI
|
|
75
|
+
* 'node_electronics_phones': 'IAB19-20', // Mobile/Phones
|
|
76
|
+
* 'node_sports_football': 'IAB17-3', // Sports > Football
|
|
77
|
+
*/
|
|
78
|
+
export const MIXPEEK_NODE_TO_IAB = {
|
|
79
|
+
// Technology & Computing (IAB19)
|
|
80
|
+
// Replace these with actual node_ids from YOUR Mixpeek taxonomy
|
|
81
|
+
'node_technology': 'IAB19',
|
|
82
|
+
'node_tech_computing': 'IAB19',
|
|
83
|
+
'node_tech_ai': 'IAB19-11',
|
|
84
|
+
'node_tech_artificial_intelligence': 'IAB19-11',
|
|
85
|
+
'node_tech_machine_learning': 'IAB19-11',
|
|
86
|
+
'node_tech_software': 'IAB19-18',
|
|
87
|
+
'node_tech_hardware': 'IAB19-19',
|
|
88
|
+
'node_tech_mobile': 'IAB19-20',
|
|
89
|
+
'node_electronics_phones': 'IAB19-20', // From OpenAPI example
|
|
90
|
+
'node_tech_internet': 'IAB19-21',
|
|
91
|
+
|
|
92
|
+
// Sports (IAB17)
|
|
93
|
+
'sports': 'IAB17',
|
|
94
|
+
'sports_football': 'IAB17-3',
|
|
95
|
+
'sports_soccer': 'IAB17-44',
|
|
96
|
+
'sports_basketball': 'IAB17-4',
|
|
97
|
+
'sports_baseball': 'IAB17-5',
|
|
98
|
+
'sports_tennis': 'IAB17-37',
|
|
99
|
+
|
|
100
|
+
// News (IAB12)
|
|
101
|
+
'news': 'IAB12',
|
|
102
|
+
'news_politics': 'IAB12-2',
|
|
103
|
+
'news_business': 'IAB12-3',
|
|
104
|
+
'news_technology': 'IAB12-6',
|
|
105
|
+
|
|
106
|
+
// Business & Finance (IAB13)
|
|
107
|
+
'business': 'IAB13',
|
|
108
|
+
'business_finance': 'IAB13-7',
|
|
109
|
+
'business_investing': 'IAB13-5',
|
|
110
|
+
|
|
111
|
+
// Entertainment (IAB9)
|
|
112
|
+
'entertainment': 'IAB9',
|
|
113
|
+
'entertainment_movies': 'IAB9-7',
|
|
114
|
+
'entertainment_tv': 'IAB9-23',
|
|
115
|
+
'entertainment_music': 'IAB9-8',
|
|
116
|
+
|
|
117
|
+
// Health & Fitness (IAB7)
|
|
118
|
+
'health': 'IAB7',
|
|
119
|
+
'health_fitness': 'IAB7-18',
|
|
120
|
+
'health_nutrition': 'IAB7-30',
|
|
121
|
+
|
|
122
|
+
// Travel (IAB20)
|
|
123
|
+
'travel': 'IAB20',
|
|
124
|
+
'travel_hotels': 'IAB20-12',
|
|
125
|
+
|
|
126
|
+
// Food & Drink (IAB8)
|
|
127
|
+
'food': 'IAB8',
|
|
128
|
+
'food_cooking': 'IAB8-5',
|
|
129
|
+
'food_restaurants': 'IAB8-9',
|
|
130
|
+
|
|
131
|
+
// Automotive (IAB2)
|
|
132
|
+
'automotive': 'IAB2',
|
|
133
|
+
'automotive_cars': 'IAB2',
|
|
134
|
+
|
|
135
|
+
// Real Estate (IAB21)
|
|
136
|
+
'real_estate': 'IAB21',
|
|
137
|
+
|
|
138
|
+
// Education (IAB5)
|
|
139
|
+
'education': 'IAB5',
|
|
140
|
+
|
|
141
|
+
// Fashion & Style (IAB18)
|
|
142
|
+
'fashion': 'IAB18',
|
|
143
|
+
'fashion_beauty': 'IAB18-1',
|
|
144
|
+
|
|
145
|
+
// Home & Garden (IAB10)
|
|
146
|
+
'home': 'IAB10',
|
|
147
|
+
'home_garden': 'IAB10-3',
|
|
148
|
+
|
|
149
|
+
// Science (IAB15)
|
|
150
|
+
'science': 'IAB15',
|
|
151
|
+
|
|
152
|
+
// Arts (IAB1)
|
|
153
|
+
'arts': 'IAB1',
|
|
154
|
+
'arts_design': 'IAB1-4'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Fallback label-based mapping (Scenario C)
|
|
159
|
+
* Used when node_id mapping fails
|
|
160
|
+
*/
|
|
161
|
+
export const LABEL_TO_IAB = {
|
|
162
|
+
// Technology patterns
|
|
163
|
+
'technology': 'IAB19',
|
|
164
|
+
'tech': 'IAB19',
|
|
165
|
+
'ai': 'IAB19-11',
|
|
166
|
+
'artificial intelligence': 'IAB19-11',
|
|
167
|
+
'machine learning': 'IAB19-11',
|
|
168
|
+
'software': 'IAB19-18',
|
|
169
|
+
'hardware': 'IAB19-19',
|
|
170
|
+
'mobile': 'IAB19-20',
|
|
171
|
+
'computing': 'IAB19',
|
|
172
|
+
|
|
173
|
+
// Sports patterns
|
|
174
|
+
'sports': 'IAB17',
|
|
175
|
+
'football': 'IAB17-3',
|
|
176
|
+
'soccer': 'IAB17-44',
|
|
177
|
+
'basketball': 'IAB17-4',
|
|
178
|
+
'baseball': 'IAB17-5',
|
|
179
|
+
|
|
180
|
+
// News patterns
|
|
181
|
+
'news': 'IAB12',
|
|
182
|
+
'politics': 'IAB12-2',
|
|
183
|
+
|
|
184
|
+
// Business patterns
|
|
185
|
+
'business': 'IAB13',
|
|
186
|
+
'finance': 'IAB13-7',
|
|
187
|
+
'investing': 'IAB13-5',
|
|
188
|
+
|
|
189
|
+
// Entertainment patterns
|
|
190
|
+
'entertainment': 'IAB9',
|
|
191
|
+
'movies': 'IAB9-7',
|
|
192
|
+
'television': 'IAB9-23',
|
|
193
|
+
'music': 'IAB9-8',
|
|
194
|
+
'gaming': 'IAB9-30',
|
|
195
|
+
|
|
196
|
+
// Health patterns
|
|
197
|
+
'health': 'IAB7',
|
|
198
|
+
'fitness': 'IAB7-18',
|
|
199
|
+
'wellness': 'IAB7',
|
|
200
|
+
'nutrition': 'IAB7-30',
|
|
201
|
+
|
|
202
|
+
// Other categories
|
|
203
|
+
'travel': 'IAB20',
|
|
204
|
+
'food': 'IAB8',
|
|
205
|
+
'automotive': 'IAB2',
|
|
206
|
+
'cars': 'IAB2',
|
|
207
|
+
'real estate': 'IAB21',
|
|
208
|
+
'education': 'IAB5',
|
|
209
|
+
'fashion': 'IAB18',
|
|
210
|
+
'home': 'IAB10',
|
|
211
|
+
'science': 'IAB15',
|
|
212
|
+
'arts': 'IAB1'
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if a string is already a valid IAB category code
|
|
217
|
+
* @param {string} value - Potential IAB code
|
|
218
|
+
* @returns {boolean} True if valid IAB code format
|
|
219
|
+
*/
|
|
220
|
+
export function isValidIABCode(value) {
|
|
221
|
+
if (!value || typeof value !== 'string') return false
|
|
222
|
+
return /^IAB\d+(-\d+)?$/.test(value)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Extract IAB code from a string if present
|
|
227
|
+
* @param {string} value - String that might contain IAB code
|
|
228
|
+
* @returns {string|null} IAB code if found, null otherwise
|
|
229
|
+
*/
|
|
230
|
+
export function extractIABCode(value) {
|
|
231
|
+
if (!value || typeof value !== 'string') return null
|
|
232
|
+
const match = value.match(/IAB\d+(-\d+)?/)
|
|
233
|
+
return match ? match[0] : null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Map Mixpeek taxonomy to IAB code using multiple strategies
|
|
238
|
+
* @param {object} taxonomy - Mixpeek taxonomy object with {label, node_id, path, score}
|
|
239
|
+
* @returns {string|null} IAB category code or null if not found
|
|
240
|
+
*/
|
|
241
|
+
export function getIABFromTaxonomy(taxonomy) {
|
|
242
|
+
if (!taxonomy) return null
|
|
243
|
+
|
|
244
|
+
// Strategy 1: Check if label is already an IAB code (Scenario A)
|
|
245
|
+
if (taxonomy.label) {
|
|
246
|
+
if (isValidIABCode(taxonomy.label)) {
|
|
247
|
+
return taxonomy.label
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check if label contains IAB code
|
|
251
|
+
const iabInLabel = extractIABCode(taxonomy.label)
|
|
252
|
+
if (iabInLabel) {
|
|
253
|
+
return iabInLabel
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Strategy 2: Check if node_id is an IAB code (Scenario A)
|
|
258
|
+
if (taxonomy.nodeId || taxonomy.node_id) {
|
|
259
|
+
const nodeId = taxonomy.nodeId || taxonomy.node_id
|
|
260
|
+
|
|
261
|
+
if (isValidIABCode(nodeId)) {
|
|
262
|
+
return nodeId
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check if node_id contains IAB code
|
|
266
|
+
const iabInNode = extractIABCode(nodeId)
|
|
267
|
+
if (iabInNode) {
|
|
268
|
+
return iabInNode
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Strategy 3: Map by Mixpeek node_id (Scenario B)
|
|
273
|
+
if (taxonomy.nodeId || taxonomy.node_id) {
|
|
274
|
+
const nodeId = (taxonomy.nodeId || taxonomy.node_id).toLowerCase()
|
|
275
|
+
|
|
276
|
+
// Direct match
|
|
277
|
+
if (MIXPEEK_NODE_TO_IAB[nodeId]) {
|
|
278
|
+
return MIXPEEK_NODE_TO_IAB[nodeId]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Try without underscores/hyphens
|
|
282
|
+
const normalizedNode = nodeId.replace(/[_-]/g, '')
|
|
283
|
+
for (const [key, value] of Object.entries(MIXPEEK_NODE_TO_IAB)) {
|
|
284
|
+
if (key.replace(/[_-]/g, '') === normalizedNode) {
|
|
285
|
+
return value
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Strategy 4: Map by label text (Scenario C - fallback)
|
|
291
|
+
if (taxonomy.label) {
|
|
292
|
+
const label = taxonomy.label.toLowerCase()
|
|
293
|
+
|
|
294
|
+
// Direct match
|
|
295
|
+
if (LABEL_TO_IAB[label]) {
|
|
296
|
+
return LABEL_TO_IAB[label]
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Try partial matches
|
|
300
|
+
for (const [key, value] of Object.entries(LABEL_TO_IAB)) {
|
|
301
|
+
if (label.includes(key)) {
|
|
302
|
+
return value
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Strategy 5: Try path if available
|
|
308
|
+
if (taxonomy.path) {
|
|
309
|
+
const pathString = Array.isArray(taxonomy.path)
|
|
310
|
+
? taxonomy.path.join(' ').toLowerCase()
|
|
311
|
+
: taxonomy.path.toLowerCase()
|
|
312
|
+
|
|
313
|
+
for (const [key, value] of Object.entries(LABEL_TO_IAB)) {
|
|
314
|
+
if (pathString.includes(key)) {
|
|
315
|
+
return value
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return null
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Map multiple taxonomies to IAB codes
|
|
325
|
+
* @param {Array<object>} taxonomies - Array of taxonomy objects
|
|
326
|
+
* @returns {Array<string>} Array of IAB codes (duplicates removed)
|
|
327
|
+
*/
|
|
328
|
+
export function mapTaxonomiesToIAB(taxonomies) {
|
|
329
|
+
if (!Array.isArray(taxonomies)) return []
|
|
330
|
+
|
|
331
|
+
const iabCodes = taxonomies
|
|
332
|
+
.map(tax => getIABFromTaxonomy(tax))
|
|
333
|
+
.filter(code => code !== null)
|
|
334
|
+
|
|
335
|
+
// Remove duplicates
|
|
336
|
+
return [...new Set(iabCodes)]
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Map categories to IAB codes (deprecated - use getIABFromTaxonomy)
|
|
341
|
+
* @deprecated Use getIABFromTaxonomy for taxonomy objects
|
|
342
|
+
* @param {Array<string>} categories - Array of category strings
|
|
343
|
+
* @returns {Array<string>} Array of IAB codes
|
|
344
|
+
*/
|
|
345
|
+
export function mapCategoriesToIAB(categories) {
|
|
346
|
+
if (!Array.isArray(categories)) return []
|
|
347
|
+
|
|
348
|
+
const iabCodes = categories
|
|
349
|
+
.map(cat => {
|
|
350
|
+
const normalized = cat.toLowerCase()
|
|
351
|
+
return LABEL_TO_IAB[normalized] || null
|
|
352
|
+
})
|
|
353
|
+
.filter(code => code !== null)
|
|
354
|
+
|
|
355
|
+
return [...new Set(iabCodes)]
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export default {
|
|
359
|
+
IAB_TAXONOMY_VERSION,
|
|
360
|
+
MIXPEEK_NODE_TO_IAB,
|
|
361
|
+
LABEL_TO_IAB,
|
|
362
|
+
isValidIABCode,
|
|
363
|
+
extractIABCode,
|
|
364
|
+
getIABFromTaxonomy,
|
|
365
|
+
mapTaxonomiesToIAB,
|
|
366
|
+
mapCategoriesToIAB
|
|
367
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mixpeek Context Adapter - Logger Utility
|
|
3
|
+
* @module utils/logger
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MIXPEEK_MODULE_NAME } from '../config/constants.js'
|
|
7
|
+
|
|
8
|
+
class Logger {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.debug = false
|
|
11
|
+
this.prefix = `[${MIXPEEK_MODULE_NAME}]`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setDebug(enabled) {
|
|
15
|
+
this.debug = enabled
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
info(...args) {
|
|
19
|
+
if (this.debug) {
|
|
20
|
+
console.log(this.prefix, ...args)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
warn(...args) {
|
|
25
|
+
console.warn(this.prefix, ...args)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
error(...args) {
|
|
29
|
+
console.error(this.prefix, ...args)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
group(label) {
|
|
33
|
+
if (this.debug && console.group) {
|
|
34
|
+
console.group(`${this.prefix} ${label}`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
groupEnd() {
|
|
39
|
+
if (this.debug && console.groupEnd) {
|
|
40
|
+
console.groupEnd()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
time(label) {
|
|
45
|
+
if (this.debug && console.time) {
|
|
46
|
+
console.time(`${this.prefix} ${label}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
timeEnd(label) {
|
|
51
|
+
if (this.debug && console.timeEnd) {
|
|
52
|
+
console.timeEnd(`${this.prefix} ${label}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
table(data) {
|
|
57
|
+
if (this.debug && console.table) {
|
|
58
|
+
console.table(data)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default new Logger()
|
|
64
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Previous Ad Tracker
|
|
3
|
+
* Stores minimal information about the most recently served ad
|
|
4
|
+
* using in-memory storage with optional localStorage persistence.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isBrowser, safeJSONParse } from './helpers.js'
|
|
8
|
+
import logger from './logger.js'
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = 'mixpeek_prev_ad_v1'
|
|
11
|
+
|
|
12
|
+
class PreviousAdTracker {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.lastAd = null
|
|
15
|
+
this.storageAvailable = this._checkLocalStorage()
|
|
16
|
+
this._loadFromStorage()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_checkLocalStorage() {
|
|
20
|
+
if (!isBrowser()) return false
|
|
21
|
+
try {
|
|
22
|
+
const k = '__mixpeek_prev_test__'
|
|
23
|
+
localStorage.setItem(k, '1')
|
|
24
|
+
localStorage.removeItem(k)
|
|
25
|
+
return true
|
|
26
|
+
} catch (e) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_loadFromStorage() {
|
|
32
|
+
if (!this.storageAvailable) return
|
|
33
|
+
try {
|
|
34
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
35
|
+
if (raw) {
|
|
36
|
+
const parsed = safeJSONParse(raw)
|
|
37
|
+
if (parsed && typeof parsed === 'object') {
|
|
38
|
+
this.lastAd = parsed
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
logger.warn('Failed to load previous ad from storage:', e)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_persist() {
|
|
47
|
+
if (!this.storageAvailable) return
|
|
48
|
+
try {
|
|
49
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.lastAd))
|
|
50
|
+
} catch (e) {
|
|
51
|
+
logger.warn('Failed to persist previous ad:', e)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Record the last served ad from a Prebid bidResponse
|
|
57
|
+
* @param {object} bidResponse
|
|
58
|
+
*/
|
|
59
|
+
record(bidResponse) {
|
|
60
|
+
if (!bidResponse || typeof bidResponse !== 'object') return
|
|
61
|
+
const info = {
|
|
62
|
+
creativeId: bidResponse.creativeId || bidResponse.creative_id || null,
|
|
63
|
+
bidder: bidResponse.bidder || bidResponse.bidderCode || null,
|
|
64
|
+
adUnitCode: bidResponse.adUnitCode || null,
|
|
65
|
+
cpm: typeof bidResponse.cpm === 'number' ? bidResponse.cpm : null,
|
|
66
|
+
currency: bidResponse.currency || null,
|
|
67
|
+
categories: Array.isArray(bidResponse.meta?.adServerCatId) ? bidResponse.meta.adServerCatId : (bidResponse.meta?.primaryCat ? [bidResponse.meta.primaryCat] : []),
|
|
68
|
+
timestamp: Date.now()
|
|
69
|
+
}
|
|
70
|
+
this.lastAd = info
|
|
71
|
+
this._persist()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get last ad info
|
|
76
|
+
* @returns {object|null}
|
|
77
|
+
*/
|
|
78
|
+
getLast() {
|
|
79
|
+
return this.lastAd
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Clear last ad
|
|
84
|
+
*/
|
|
85
|
+
clear() {
|
|
86
|
+
this.lastAd = null
|
|
87
|
+
if (this.storageAvailable) {
|
|
88
|
+
try { localStorage.removeItem(STORAGE_KEY) } catch (e) {}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default new PreviousAdTracker()
|
|
94
|
+
|
|
95
|
+
|