@mixpeek/prebid 1.0.0 → 1.0.2

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.
@@ -40,7 +40,7 @@ pbjs.setConfig({
40
40
  mixpeek: {
41
41
  apiKey: 'sk_your_api_key',
42
42
  collectionId: 'col_your_collection',
43
- endpoint: 'https://server-xb24.onrender.com',
43
+ endpoint: 'https://api.mixpeek.com',
44
44
  namespace: 'production',
45
45
  featureExtractors: ['taxonomy', 'brand-safety'],
46
46
  mode: 'auto',
@@ -63,7 +63,7 @@ pbjs.setConfig({
63
63
  params: { // NEW: Wrap all config in params
64
64
  apiKey: 'sk_your_api_key',
65
65
  collectionId: 'col_your_collection',
66
- endpoint: 'https://server-xb24.onrender.com',
66
+ endpoint: 'https://api.mixpeek.com',
67
67
  namespace: 'production',
68
68
  featureExtractors: ['taxonomy', 'brand-safety'],
69
69
  mode: 'auto',
@@ -427,7 +427,7 @@ pbjs.setConfig({
427
427
  ### 2. Revert Package
428
428
 
429
429
  ```bash
430
- npm install @mixpeek/prebid-contextual-adapter@1.x
430
+ npm install @mixpeek/prebid@1.x
431
431
  ```
432
432
 
433
433
  ### 3. Clear Cache
@@ -487,7 +487,7 @@ Need help with migration?
487
487
 
488
488
  - **Documentation:** [Full Integration Guide](integration-guide.md)
489
489
  - **Examples:** See `examples/` directory
490
- - **Issues:** [GitHub Issues](https://github.com/mixpeek/prebid-contextual-adapter/issues)
490
+ - **Issues:** [GitHub Issues](https://github.com/mixpeek/prebid/issues)
491
491
  - **Email:** support@mixpeek.com
492
492
  - **Slack:** [Join our Slack](https://mixpeek.com/slack)
493
493
 
@@ -451,5 +451,5 @@ For questions or issues:
451
451
 
452
452
  - **Documentation**: [docs.mixpeek.com](https://docs.mixpeek.com)
453
453
  - **Email**: support@mixpeek.com
454
- - **GitHub**: [github.com/mixpeek/prebid-contextual-adapter](https://github.com/mixpeek/prebid-contextual-adapter)
454
+ - **GitHub**: [github.com/mixpeek/prebid](https://github.com/mixpeek/prebid)
455
455
 
@@ -164,7 +164,7 @@ if (health.status === 'ok') {
164
164
  ```javascript
165
165
  pbjs.setConfig({
166
166
  mixpeek: {
167
- endpoint: 'https://server-xb24.onrender.com',
167
+ endpoint: 'https://api.mixpeek.com',
168
168
  healthCheck: 'eager', // Validate immediately
169
169
  debug: true,
170
170
  timeout: 5000
@@ -177,7 +177,7 @@ pbjs.setConfig({
177
177
  ```javascript
178
178
  pbjs.setConfig({
179
179
  mixpeek: {
180
- endpoint: 'https://server-xb24.onrender.com',
180
+ endpoint: 'https://api.mixpeek.com',
181
181
  healthCheck: 'lazy', // Balance validation & performance
182
182
  debug: true,
183
183
  timeout: 3000
@@ -251,11 +251,11 @@ This ensures **resilient behavior** where API issues never break your ads.
251
251
 
252
252
  ```bash
253
253
  # Test endpoint manually
254
- curl https://server-xb24.onrender.com/v1/health
254
+ curl https://api.mixpeek.com/v1/health
255
255
 
256
256
  # With authentication
257
257
  curl -H "Authorization: Bearer YOUR_API_KEY" \
258
- https://server-xb24.onrender.com/v1/health
258
+ https://api.mixpeek.com/v1/health
259
259
  ```
260
260
 
261
261
  ### Enable Debug Logging
@@ -272,7 +272,7 @@ pbjs.setConfig({
272
272
  Console output:
273
273
  ```
274
274
  [mixpeek] Performing health check...
275
- [mixpeek] API Request: GET https://server-xb24.onrender.com/v1/health
275
+ [mixpeek] API Request: GET https://api.mixpeek.com/v1/health
276
276
  [mixpeek] API Response: { status: 200, ... }
277
277
  [mixpeek] Health check passed: API responding in 234ms
278
278
  ```
@@ -46,24 +46,24 @@ Before you begin, make sure you have:
46
46
  ### Option 1: NPM (Recommended)
47
47
 
48
48
  ```bash
49
- npm install @mixpeek/prebid-contextual-adapter
49
+ npm install @mixpeek/prebid
50
50
  ```
51
51
 
52
52
  Then include in your JavaScript:
53
53
 
54
54
  ```javascript
55
- import '@mixpeek/prebid-contextual-adapter'
55
+ import '@mixpeek/prebid'
56
56
  ```
57
57
 
58
58
  ### Option 2: CDN
59
59
 
60
60
  ```html
61
- <script src="https://cdn.jsdelivr.net/npm/@mixpeek/prebid-contextual-adapter@latest/dist/mixpeekContextAdapter.js"></script>
61
+ <script src="https://cdn.jsdelivr.net/npm/@mixpeek/prebid@latest/dist/mixpeekContextAdapter.js"></script>
62
62
  ```
63
63
 
64
64
  ### Option 3: Download
65
65
 
66
- Download the latest release from [GitHub releases](https://github.com/mixpeek/prebid-contextual-adapter/releases) and include it in your page:
66
+ Download the latest release from [GitHub releases](https://github.com/mixpeek/prebid/releases) and include it in your page:
67
67
 
68
68
  ```html
69
69
  <script src="/path/to/mixpeekContextAdapter.js"></script>
@@ -566,7 +566,7 @@ pbjs.setConfig({ mixpeek: {...} }); // Too early
566
566
 
567
567
  - **Documentation**: [docs.mixpeek.com](https://docs.mixpeek.com)
568
568
  - **Email**: support@mixpeek.com
569
- - **GitHub Issues**: [github.com/mixpeek/prebid-contextual-adapter/issues](https://github.com/mixpeek/prebid-contextual-adapter/issues)
569
+ - **GitHub Issues**: [github.com/mixpeek/prebid/issues](https://github.com/mixpeek/prebid/issues)
570
570
  - **Slack Community**: [Join our Slack](https://mixpeek.com/slack)
571
571
 
572
572
  ## Next Steps
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mixpeek/prebid",
3
- "version": "1.0.0",
4
- "description": "Mixpeek for Prebid.js - Enrich bid requests with real-time multimodal AI contextual data",
3
+ "version": "1.0.2",
4
+ "description": "Mixpeek RTD (Real-Time Data) Adapter for Prebid.js - Privacy-first contextual targeting with sub-100ms performance, ad adjacency awareness, and cookie-free bid enrichment",
5
5
  "main": "dist/mixpeekContextAdapter.js",
6
6
  "module": "src/modules/mixpeekContextAdapter.js",
7
7
  "scripts": {
@@ -37,20 +37,26 @@
37
37
  ],
38
38
  "keywords": [
39
39
  "prebid",
40
+ "rtd",
41
+ "real-time-data",
40
42
  "contextual",
41
43
  "advertising",
44
+ "header-bidding",
45
+ "cookie-free",
46
+ "privacy",
42
47
  "mixpeek",
43
48
  "ai",
44
49
  "multimodal",
45
50
  "iab",
46
51
  "taxonomy",
47
- "brand-safety"
52
+ "brand-safety",
53
+ "adjacency"
48
54
  ],
49
55
  "author": "Mixpeek",
50
56
  "license": "Apache-2.0",
51
57
  "repository": {
52
58
  "type": "git",
53
- "url": "https://github.com/mixpeek/prebid.git"
59
+ "url": "git+https://github.com/mixpeek/prebid.git"
54
60
  },
55
61
  "bugs": {
56
62
  "url": "https://github.com/mixpeek/prebid/issues"
@@ -35,16 +35,18 @@ class MixpeekClient {
35
35
  /**
36
36
  * Build headers for API request
37
37
  * @private
38
+ * @param {boolean} requireNamespace - Whether namespace header is required
38
39
  * @returns {object} Headers object
39
40
  */
40
- _buildHeaders() {
41
+ _buildHeaders(requireNamespace = true) {
41
42
  const headers = {
42
43
  [HEADERS.CONTENT_TYPE]: 'application/json',
43
44
  [HEADERS.AUTHORIZATION]: `Bearer ${this.apiKey}`,
44
45
  [HEADERS.USER_AGENT]: USER_AGENT
45
46
  }
46
47
 
47
- if (this.namespace) {
48
+ // Add namespace header (required for most endpoints)
49
+ if (this.namespace && requireNamespace) {
48
50
  headers[HEADERS.NAMESPACE] = this.namespace
49
51
  }
50
52
 
@@ -135,11 +137,16 @@ class MixpeekClient {
135
137
  */
136
138
  async createDocument(collectionId, payload) {
137
139
  const path = ENDPOINTS.DOCUMENTS.replace('{collectionId}', collectionId)
138
-
140
+
141
+ // Build request payload according to Mixpeek API spec
139
142
  const requestPayload = {
140
- object_id: payload.objectId || generateUUID(),
141
- metadata: payload.metadata || {},
142
- features: payload.features || []
143
+ collection_id: collectionId,
144
+ ...payload.metadata
145
+ }
146
+
147
+ // Add content field if provided
148
+ if (payload.content) {
149
+ requestPayload.content = payload.content
143
150
  }
144
151
 
145
152
  return retryWithBackoff(
@@ -166,52 +173,146 @@ class MixpeekClient {
166
173
  * Process content with feature extractors
167
174
  * @param {string} collectionId - Collection ID
168
175
  * @param {object} content - Content to process
169
- * @param {array} featureExtractors - Feature extractors to use
170
- * @returns {Promise} Enriched document
176
+ * @param {array} featureExtractors - Feature extractors to use (optional, for future use)
177
+ * @returns {Promise} Enriched document with context data
171
178
  */
172
179
  async processContent(collectionId, content, featureExtractors = []) {
173
180
  logger.group('Processing content with Mixpeek')
174
181
  logger.info('Collection:', collectionId)
175
- logger.info('Feature Extractors:', featureExtractors)
182
+ logger.info('Content URL:', content.url)
176
183
 
177
184
  try {
178
- // Build features array from extractors
179
- const features = featureExtractors.map(extractor => {
180
- const feature = {
181
- feature_extractor_id: typeof extractor === 'string' ? extractor : extractor.feature_extractor_id
182
- }
183
-
184
- // Add payload if provided
185
- if (typeof extractor === 'object' && extractor.payload) {
186
- feature.payload = extractor.payload
187
- } else {
188
- // Build payload from content
189
- feature.payload = this._buildFeaturePayload(content)
190
- }
191
-
192
- return feature
193
- })
194
-
195
- // Create document with features
185
+ // Create document in collection
196
186
  const document = await this.createDocument(collectionId, {
197
- objectId: this._generateContentId(content),
187
+ content: content.text || content.description || '',
198
188
  metadata: {
199
189
  url: content.url,
200
190
  title: content.title,
201
191
  timestamp: Date.now()
202
- },
203
- features
192
+ }
204
193
  })
205
194
 
206
195
  logger.info('Document created:', document.document_id)
196
+
197
+ // Build enrichments from content analysis (client-side fallback)
198
+ // In future versions, this will use Mixpeek's taxonomy and classification APIs
199
+ const enrichments = this._buildLocalEnrichments(content)
200
+
207
201
  logger.groupEnd()
208
202
 
209
- return document
203
+ return {
204
+ document_id: document.document_id,
205
+ collection_id: document.collection_id,
206
+ enrichments
207
+ }
210
208
  } catch (error) {
211
209
  logger.error('Error processing content:', error)
212
210
  logger.groupEnd()
213
- throw error
211
+
212
+ // Return fallback enrichments on API error (graceful degradation)
213
+ return {
214
+ document_id: null,
215
+ collection_id: collectionId,
216
+ enrichments: this._buildLocalEnrichments(content)
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Build local enrichments from content (client-side analysis)
223
+ * This provides basic contextual data when API processing is unavailable
224
+ * @private
225
+ * @param {object} content - Content object
226
+ * @returns {object} Enrichments object
227
+ */
228
+ _buildLocalEnrichments(content) {
229
+ const enrichments = {}
230
+
231
+ // Extract keywords from content
232
+ if (content.text) {
233
+ enrichments.keywords = this._extractKeywords(content.text)
234
+ }
235
+
236
+ // Analyze sentiment (basic)
237
+ if (content.text) {
238
+ enrichments.sentiment = this._analyzeSentiment(content.text)
214
239
  }
240
+
241
+ // Generate content hash as embedding ID
242
+ enrichments.embeddings = [{
243
+ id: `emb_${this._generateContentId(content)}`
244
+ }]
245
+
246
+ return enrichments
247
+ }
248
+
249
+ /**
250
+ * Extract keywords from text (simple implementation)
251
+ * @private
252
+ * @param {string} text - Text content
253
+ * @returns {array} Array of keywords
254
+ */
255
+ _extractKeywords(text) {
256
+ if (!text) return []
257
+
258
+ // Simple keyword extraction: common important words
259
+ const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare', 'ought', 'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'just', 'don', 'now', 'and', 'but', 'or', 'if', 'this', 'that', 'these', 'those', 'it', 'its'])
260
+
261
+ const words = text.toLowerCase()
262
+ .replace(/[^\w\s]/g, ' ')
263
+ .split(/\s+/)
264
+ .filter(word => word.length > 3 && !stopWords.has(word))
265
+
266
+ // Count word frequency
267
+ const wordCount = {}
268
+ words.forEach(word => {
269
+ wordCount[word] = (wordCount[word] || 0) + 1
270
+ })
271
+
272
+ // Return top 10 keywords by frequency
273
+ return Object.entries(wordCount)
274
+ .sort((a, b) => b[1] - a[1])
275
+ .slice(0, 10)
276
+ .map(([word]) => word)
277
+ }
278
+
279
+ /**
280
+ * Basic sentiment analysis
281
+ * @private
282
+ * @param {string} text - Text content
283
+ * @returns {object} Sentiment result
284
+ */
285
+ _analyzeSentiment(text) {
286
+ if (!text) return { label: 'neutral', score: 0.5 }
287
+
288
+ const positiveWords = ['good', 'great', 'excellent', 'amazing', 'wonderful', 'fantastic', 'best', 'love', 'happy', 'positive', 'success', 'win', 'awesome', 'brilliant', 'perfect', 'beautiful', 'enjoy', 'exciting']
289
+ const negativeWords = ['bad', 'terrible', 'awful', 'horrible', 'worst', 'hate', 'sad', 'negative', 'fail', 'loss', 'poor', 'ugly', 'boring', 'disappointing', 'wrong', 'problem', 'issue', 'error']
290
+
291
+ const lowerText = text.toLowerCase()
292
+ let positiveCount = 0
293
+ let negativeCount = 0
294
+
295
+ positiveWords.forEach(word => {
296
+ const regex = new RegExp(`\\b${word}\\b`, 'gi')
297
+ const matches = lowerText.match(regex)
298
+ if (matches) positiveCount += matches.length
299
+ })
300
+
301
+ negativeWords.forEach(word => {
302
+ const regex = new RegExp(`\\b${word}\\b`, 'gi')
303
+ const matches = lowerText.match(regex)
304
+ if (matches) negativeCount += matches.length
305
+ })
306
+
307
+ const total = positiveCount + negativeCount
308
+ if (total === 0) return { label: 'neutral', score: 0.5 }
309
+
310
+ const score = positiveCount / total
311
+ let label = 'neutral'
312
+ if (score > 0.6) label = 'positive'
313
+ else if (score < 0.4) label = 'negative'
314
+
315
+ return { label, score }
215
316
  }
216
317
 
217
318
  /**
@@ -17,7 +17,7 @@ export const DEFAULT_API_ENDPOINT = typeof process !== 'undefined' && process.en
17
17
  // Alternative endpoints
18
18
  export const API_ENDPOINTS = {
19
19
  PRODUCTION: 'https://api.mixpeek.com',
20
- DEVELOPMENT: 'https://server-xb24.onrender.com',
20
+ DEVELOPMENT: 'https://api.mixpeek.com',
21
21
  LOCAL: 'http://localhost:8000'
22
22
  }
23
23
 
@@ -43,14 +43,19 @@ export const CONTENT_MODES = {
43
43
  IMAGE: 'image'
44
44
  }
45
45
 
46
- // Feature Extractors
46
+ // Feature Extractors (actual Mixpeek API extractors)
47
47
  export const FEATURE_EXTRACTORS = {
48
- TAXONOMY: 'taxonomy',
49
- BRAND_SAFETY: 'brand-safety',
50
- KEYWORDS: 'keywords',
51
- SENTIMENT: 'sentiment',
52
- CLUSTERING: 'clustering',
53
- EMBEDDING: 'embedding'
48
+ // Actual Mixpeek extractors
49
+ TEXT: 'text_extractor_v1',
50
+ SENTIMENT: 'sentiment_classifier_v1',
51
+ IMAGE: 'image_extractor_v1',
52
+ MULTIMODAL: 'multimodal_extractor_v1',
53
+ // Legacy aliases for backwards compatibility
54
+ TAXONOMY: 'text_extractor_v1',
55
+ BRAND_SAFETY: 'sentiment_classifier_v1',
56
+ KEYWORDS: 'text_extractor_v1',
57
+ CLUSTERING: 'text_extractor_v1',
58
+ EMBEDDING: 'text_extractor_v1'
54
59
  }
55
60
 
56
61
  // Targeting Key Prefixes
@@ -452,10 +452,10 @@ class MixpeekContextAdapter {
452
452
  }
453
453
  }
454
454
 
455
- // Extract taxonomies
455
+ // Extract taxonomies (if available from Mixpeek classification)
456
456
  if (document.enrichments && document.enrichments.taxonomies) {
457
457
  const taxonomies = document.enrichments.taxonomies
458
-
458
+
459
459
  if (taxonomies.length > 0) {
460
460
  const primaryTaxonomy = taxonomies[0]
461
461
  context.taxonomy = {
@@ -470,9 +470,18 @@ class MixpeekContextAdapter {
470
470
 
471
471
  // Extract other enrichments
472
472
  if (document.enrichments) {
473
- // Brand safety
473
+ // Brand safety (use sentiment as proxy for now)
474
474
  if (document.enrichments.brand_safety) {
475
475
  context.brandSafety = document.enrichments.brand_safety
476
+ } else if (document.enrichments.sentiment) {
477
+ // Use positive sentiment as a proxy for brand safety
478
+ const sentimentScore = typeof document.enrichments.sentiment === 'object'
479
+ ? document.enrichments.sentiment.score
480
+ : 0.5
481
+ context.brandSafety = {
482
+ score: sentimentScore > 0.5 ? 0.8 + (sentimentScore - 0.5) * 0.4 : 0.5 + sentimentScore * 0.6,
483
+ level: sentimentScore > 0.6 ? 'safe' : sentimentScore < 0.4 ? 'caution' : 'neutral'
484
+ }
476
485
  }
477
486
 
478
487
  // Keywords
@@ -491,9 +500,52 @@ class MixpeekContextAdapter {
491
500
  }
492
501
  }
493
502
 
503
+ // Build a taxonomy-like structure from keywords if no taxonomy available
504
+ if (!context.taxonomy && context.keywords && context.keywords.length > 0) {
505
+ context.taxonomy = {
506
+ label: this._inferCategoryFromKeywords(context.keywords),
507
+ nodeId: `kw_${context.keywords[0]}`,
508
+ path: ['Content', this._inferCategoryFromKeywords(context.keywords)],
509
+ score: 0.7 // Moderate confidence for keyword-based classification
510
+ }
511
+ }
512
+
494
513
  return context
495
514
  }
496
515
 
516
+ /**
517
+ * Infer a category from keywords (simple heuristic)
518
+ * @private
519
+ * @param {array} keywords - Extracted keywords
520
+ * @returns {string} Inferred category
521
+ */
522
+ _inferCategoryFromKeywords(keywords) {
523
+ const categoryKeywords = {
524
+ 'Technology': ['technology', 'software', 'computer', 'digital', 'tech', 'programming', 'code', 'developer', 'app', 'mobile', 'phone', 'smartphone'],
525
+ 'Business': ['business', 'company', 'market', 'finance', 'investment', 'stock', 'economy', 'corporate', 'startup', 'entrepreneur'],
526
+ 'Sports': ['sports', 'game', 'team', 'player', 'football', 'basketball', 'soccer', 'baseball', 'tennis', 'golf', 'match'],
527
+ 'Entertainment': ['movie', 'film', 'music', 'celebrity', 'actor', 'singer', 'show', 'concert', 'entertainment', 'tv', 'television'],
528
+ 'Health': ['health', 'medical', 'doctor', 'hospital', 'medicine', 'disease', 'fitness', 'wellness', 'diet', 'nutrition'],
529
+ 'News': ['news', 'breaking', 'report', 'politics', 'government', 'election', 'policy', 'world', 'international'],
530
+ 'Science': ['science', 'research', 'study', 'experiment', 'discovery', 'scientist', 'physics', 'chemistry', 'biology'],
531
+ 'Automotive': ['car', 'vehicle', 'auto', 'automotive', 'driving', 'electric', 'engine', 'motor', 'truck'],
532
+ 'Travel': ['travel', 'vacation', 'hotel', 'flight', 'destination', 'tourism', 'trip', 'adventure'],
533
+ 'Food': ['food', 'recipe', 'cooking', 'restaurant', 'cuisine', 'chef', 'meal', 'dinner', 'lunch']
534
+ }
535
+
536
+ const lowerKeywords = keywords.map(k => k.toLowerCase())
537
+
538
+ for (const [category, catKeywords] of Object.entries(categoryKeywords)) {
539
+ for (const keyword of lowerKeywords) {
540
+ if (catKeywords.includes(keyword)) {
541
+ return category
542
+ }
543
+ }
544
+ }
545
+
546
+ return 'General'
547
+ }
548
+
497
549
  /**
498
550
  * Inject targeting keys into ad units
499
551
  * @private