@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.
@@ -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
+