@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,303 @@
1
+ /**
2
+ * Mixpeek Context Adapter - API Client
3
+ * @module api/mixpeekClient
4
+ */
5
+
6
+ import {
7
+ DEFAULT_API_ENDPOINT,
8
+ DEFAULT_TIMEOUT,
9
+ DEFAULT_RETRY_ATTEMPTS,
10
+ ENDPOINTS,
11
+ HEADERS,
12
+ USER_AGENT,
13
+ ERROR_CODES
14
+ } from '../config/constants.js'
15
+ import { withTimeout, retryWithBackoff, generateUUID } from '../utils/helpers.js'
16
+ import logger from '../utils/logger.js'
17
+
18
+ class MixpeekClient {
19
+ constructor(config = {}) {
20
+ this.apiKey = config.apiKey || ''
21
+ this.endpoint = config.endpoint || DEFAULT_API_ENDPOINT
22
+ this.namespace = config.namespace || null
23
+ this.timeout = config.timeout || DEFAULT_TIMEOUT
24
+ this.retryAttempts = config.retryAttempts || DEFAULT_RETRY_ATTEMPTS
25
+ }
26
+
27
+ /**
28
+ * Configure the client
29
+ * @param {object} config - Configuration object
30
+ */
31
+ configure(config) {
32
+ Object.assign(this, config)
33
+ }
34
+
35
+ /**
36
+ * Build headers for API request
37
+ * @private
38
+ * @returns {object} Headers object
39
+ */
40
+ _buildHeaders() {
41
+ const headers = {
42
+ [HEADERS.CONTENT_TYPE]: 'application/json',
43
+ [HEADERS.AUTHORIZATION]: `Bearer ${this.apiKey}`,
44
+ [HEADERS.USER_AGENT]: USER_AGENT
45
+ }
46
+
47
+ if (this.namespace) {
48
+ headers[HEADERS.NAMESPACE] = this.namespace
49
+ }
50
+
51
+ return headers
52
+ }
53
+
54
+ /**
55
+ * Make API request
56
+ * @private
57
+ * @param {string} path - API path
58
+ * @param {object} options - Fetch options
59
+ * @returns {Promise} Response data
60
+ */
61
+ async _request(path, options = {}) {
62
+ const url = `${this.endpoint}${path}`
63
+ const headers = this._buildHeaders()
64
+
65
+ const fetchOptions = {
66
+ ...options,
67
+ headers: {
68
+ ...headers,
69
+ ...options.headers
70
+ }
71
+ }
72
+
73
+ logger.info(`API Request: ${options.method || 'GET'} ${url}`)
74
+ logger.time(`API Request: ${path}`)
75
+
76
+ try {
77
+ const response = await withTimeout(
78
+ fetch(url, fetchOptions),
79
+ this.timeout
80
+ )
81
+
82
+ logger.timeEnd(`API Request: ${path}`)
83
+
84
+ if (!response.ok) {
85
+ const error = await this._handleErrorResponse(response)
86
+ throw error
87
+ }
88
+
89
+ const data = await response.json()
90
+ logger.info('API Response:', { status: response.status, path })
91
+ return data
92
+ } catch (error) {
93
+ logger.timeEnd(`API Request: ${path}`)
94
+
95
+ if (error.message === 'Timeout') {
96
+ throw {
97
+ code: ERROR_CODES.API_TIMEOUT,
98
+ message: `Request timeout after ${this.timeout}ms`,
99
+ path
100
+ }
101
+ }
102
+
103
+ throw error
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Handle error response
109
+ * @private
110
+ * @param {Response} response - Fetch response
111
+ * @returns {object} Error object
112
+ */
113
+ async _handleErrorResponse(response) {
114
+ let errorMessage = `API error: ${response.status} ${response.statusText}`
115
+
116
+ try {
117
+ const errorData = await response.json()
118
+ errorMessage = errorData.message || errorData.error || errorMessage
119
+ } catch (e) {
120
+ // Response body is not JSON
121
+ }
122
+
123
+ return {
124
+ code: ERROR_CODES.API_ERROR,
125
+ message: errorMessage,
126
+ status: response.status
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Create a document in a collection
132
+ * @param {string} collectionId - Collection ID
133
+ * @param {object} payload - Document payload
134
+ * @returns {Promise} Document data
135
+ */
136
+ async createDocument(collectionId, payload) {
137
+ const path = ENDPOINTS.DOCUMENTS.replace('{collectionId}', collectionId)
138
+
139
+ const requestPayload = {
140
+ object_id: payload.objectId || generateUUID(),
141
+ metadata: payload.metadata || {},
142
+ features: payload.features || []
143
+ }
144
+
145
+ return retryWithBackoff(
146
+ () => this._request(path, {
147
+ method: 'POST',
148
+ body: JSON.stringify(requestPayload)
149
+ }),
150
+ this.retryAttempts
151
+ )
152
+ }
153
+
154
+ /**
155
+ * Get document by ID
156
+ * @param {string} collectionId - Collection ID
157
+ * @param {string} documentId - Document ID
158
+ * @returns {Promise} Document data
159
+ */
160
+ async getDocument(collectionId, documentId) {
161
+ const path = `${ENDPOINTS.DOCUMENTS.replace('{collectionId}', collectionId)}/${documentId}`
162
+ return this._request(path, { method: 'GET' })
163
+ }
164
+
165
+ /**
166
+ * Process content with feature extractors
167
+ * @param {string} collectionId - Collection ID
168
+ * @param {object} content - Content to process
169
+ * @param {array} featureExtractors - Feature extractors to use
170
+ * @returns {Promise} Enriched document
171
+ */
172
+ async processContent(collectionId, content, featureExtractors = []) {
173
+ logger.group('Processing content with Mixpeek')
174
+ logger.info('Collection:', collectionId)
175
+ logger.info('Feature Extractors:', featureExtractors)
176
+
177
+ 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
196
+ const document = await this.createDocument(collectionId, {
197
+ objectId: this._generateContentId(content),
198
+ metadata: {
199
+ url: content.url,
200
+ title: content.title,
201
+ timestamp: Date.now()
202
+ },
203
+ features
204
+ })
205
+
206
+ logger.info('Document created:', document.document_id)
207
+ logger.groupEnd()
208
+
209
+ return document
210
+ } catch (error) {
211
+ logger.error('Error processing content:', error)
212
+ logger.groupEnd()
213
+ throw error
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Build feature payload from content
219
+ * @private
220
+ * @param {object} content - Content object
221
+ * @returns {object} Feature payload
222
+ */
223
+ _buildFeaturePayload(content) {
224
+ const payload = {}
225
+
226
+ // Add text content
227
+ if (content.text) {
228
+ payload.text = content.text
229
+ }
230
+
231
+ // Add URL
232
+ if (content.url) {
233
+ payload.url = content.url
234
+ }
235
+
236
+ // Add video URL
237
+ if (content.src && content.duration !== undefined) {
238
+ // This is video content
239
+ payload.video_url = content.src
240
+ payload.title = content.title
241
+ payload.description = content.description
242
+ }
243
+
244
+ // Add image URL
245
+ if (content.src && content.width && content.height && !content.duration) {
246
+ // This is image content
247
+ payload.image_url = content.src
248
+ payload.alt_text = content.alt
249
+ }
250
+
251
+ // Add metadata
252
+ if (content.metadata) {
253
+ payload.metadata = content.metadata
254
+ }
255
+
256
+ return payload
257
+ }
258
+
259
+ /**
260
+ * Generate content ID for caching
261
+ * @private
262
+ * @param {object} content - Content object
263
+ * @returns {string} Content ID
264
+ */
265
+ _generateContentId(content) {
266
+ if (content.url) {
267
+ return `url_${content.url.split('?')[0]}` // Remove query params
268
+ }
269
+ if (content.src) {
270
+ return `src_${content.src.split('?')[0]}`
271
+ }
272
+ return generateUUID()
273
+ }
274
+
275
+ /**
276
+ * List available feature extractors
277
+ * @returns {Promise} Feature extractors list
278
+ */
279
+ async listFeatureExtractors() {
280
+ return this._request(ENDPOINTS.FEATURE_EXTRACTORS, { method: 'GET' })
281
+ }
282
+
283
+ /**
284
+ * Get feature extractor details
285
+ * @param {string} extractorId - Feature extractor ID
286
+ * @returns {Promise} Feature extractor details
287
+ */
288
+ async getFeatureExtractor(extractorId) {
289
+ const path = ENDPOINTS.FEATURE_EXTRACTORS + `/${extractorId}`
290
+ return this._request(path, { method: 'GET' })
291
+ }
292
+
293
+ /**
294
+ * Health check
295
+ * @returns {Promise} Health status
296
+ */
297
+ async healthCheck() {
298
+ return this._request('/v1/health', { method: 'GET' })
299
+ }
300
+ }
301
+
302
+ export default MixpeekClient
303
+
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Mixpeek Context Adapter - Cache Manager
3
+ * @module cache/cacheManager
4
+ */
5
+
6
+ import { CACHE_KEYS, DEFAULT_CACHE_TTL } from '../config/constants.js'
7
+ import { getTimestamp, isExpired, safeJSONParse } from '../utils/helpers.js'
8
+ import logger from '../utils/logger.js'
9
+
10
+ class CacheManager {
11
+ constructor() {
12
+ this.memoryCache = new Map()
13
+ this.useLocalStorage = this._checkLocalStorageAvailable()
14
+ this.ttl = DEFAULT_CACHE_TTL
15
+ }
16
+
17
+ /**
18
+ * Check if localStorage is available
19
+ * @private
20
+ * @returns {boolean}
21
+ */
22
+ _checkLocalStorageAvailable() {
23
+ try {
24
+ const test = '__mixpeek_test__'
25
+ localStorage.setItem(test, test)
26
+ localStorage.removeItem(test)
27
+ return true
28
+ } catch (e) {
29
+ logger.warn('localStorage not available, using memory cache only')
30
+ return false
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Generate cache key
36
+ * @private
37
+ * @param {string} key - Key
38
+ * @returns {string} Cache key
39
+ */
40
+ _getCacheKey(key) {
41
+ return `${CACHE_KEYS.PREFIX}${CACHE_KEYS.VERSION}_${key}`
42
+ }
43
+
44
+ /**
45
+ * Set TTL
46
+ * @param {number} ttl - TTL in seconds
47
+ */
48
+ setTTL(ttl) {
49
+ this.ttl = ttl
50
+ }
51
+
52
+ /**
53
+ * Get item from cache
54
+ * @param {string} key - Cache key
55
+ * @returns {object|null} Cached value or null
56
+ */
57
+ get(key) {
58
+ const cacheKey = this._getCacheKey(key)
59
+
60
+ // Try memory cache first
61
+ if (this.memoryCache.has(cacheKey)) {
62
+ const item = this.memoryCache.get(cacheKey)
63
+ if (!isExpired(item.timestamp, this.ttl)) {
64
+ logger.info('Cache hit (memory):', key)
65
+ return item.data
66
+ } else {
67
+ logger.info('Cache expired (memory):', key)
68
+ this.memoryCache.delete(cacheKey)
69
+ }
70
+ }
71
+
72
+ // Try localStorage
73
+ if (this.useLocalStorage) {
74
+ try {
75
+ const item = localStorage.getItem(cacheKey)
76
+ if (item) {
77
+ const parsed = safeJSONParse(item)
78
+ if (parsed && !isExpired(parsed.timestamp, this.ttl)) {
79
+ logger.info('Cache hit (localStorage):', key)
80
+ // Promote to memory cache
81
+ this.memoryCache.set(cacheKey, parsed)
82
+ return parsed.data
83
+ } else {
84
+ logger.info('Cache expired (localStorage):', key)
85
+ localStorage.removeItem(cacheKey)
86
+ }
87
+ }
88
+ } catch (e) {
89
+ logger.warn('Error reading from localStorage:', e)
90
+ }
91
+ }
92
+
93
+ logger.info('Cache miss:', key)
94
+ return null
95
+ }
96
+
97
+ /**
98
+ * Set item in cache
99
+ * @param {string} key - Cache key
100
+ * @param {*} data - Data to cache
101
+ * @returns {boolean} Success
102
+ */
103
+ set(key, data) {
104
+ const cacheKey = this._getCacheKey(key)
105
+ const item = {
106
+ data,
107
+ timestamp: getTimestamp()
108
+ }
109
+
110
+ try {
111
+ // Store in memory cache
112
+ this.memoryCache.set(cacheKey, item)
113
+
114
+ // Store in localStorage
115
+ if (this.useLocalStorage) {
116
+ localStorage.setItem(cacheKey, JSON.stringify(item))
117
+ }
118
+
119
+ logger.info('Cached:', key)
120
+ return true
121
+ } catch (e) {
122
+ logger.warn('Error setting cache:', e)
123
+ return false
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Remove item from cache
129
+ * @param {string} key - Cache key
130
+ * @returns {boolean} Success
131
+ */
132
+ remove(key) {
133
+ const cacheKey = this._getCacheKey(key)
134
+
135
+ try {
136
+ this.memoryCache.delete(cacheKey)
137
+ if (this.useLocalStorage) {
138
+ localStorage.removeItem(cacheKey)
139
+ }
140
+ logger.info('Cache removed:', key)
141
+ return true
142
+ } catch (e) {
143
+ logger.warn('Error removing cache:', e)
144
+ return false
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Clear all cache
150
+ * @returns {boolean} Success
151
+ */
152
+ clear() {
153
+ try {
154
+ this.memoryCache.clear()
155
+
156
+ if (this.useLocalStorage) {
157
+ const keys = Object.keys(localStorage)
158
+ keys.forEach(key => {
159
+ if (key.startsWith(CACHE_KEYS.PREFIX)) {
160
+ localStorage.removeItem(key)
161
+ }
162
+ })
163
+ }
164
+
165
+ logger.info('Cache cleared')
166
+ return true
167
+ } catch (e) {
168
+ logger.warn('Error clearing cache:', e)
169
+ return false
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Get cache stats
175
+ * @returns {object} Cache statistics
176
+ */
177
+ getStats() {
178
+ let localStorageSize = 0
179
+ let localStorageCount = 0
180
+
181
+ if (this.useLocalStorage) {
182
+ try {
183
+ const keys = Object.keys(localStorage)
184
+ keys.forEach(key => {
185
+ if (key.startsWith(CACHE_KEYS.PREFIX)) {
186
+ localStorageCount++
187
+ localStorageSize += localStorage.getItem(key).length
188
+ }
189
+ })
190
+ } catch (e) {
191
+ logger.warn('Error getting cache stats:', e)
192
+ }
193
+ }
194
+
195
+ return {
196
+ memoryCount: this.memoryCache.size,
197
+ localStorageCount,
198
+ localStorageSize,
199
+ ttl: this.ttl
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Prune expired items
205
+ * @returns {number} Number of items pruned
206
+ */
207
+ prune() {
208
+ let pruned = 0
209
+
210
+ // Prune memory cache
211
+ for (const [key, item] of this.memoryCache.entries()) {
212
+ if (isExpired(item.timestamp, this.ttl)) {
213
+ this.memoryCache.delete(key)
214
+ pruned++
215
+ }
216
+ }
217
+
218
+ // Prune localStorage
219
+ if (this.useLocalStorage) {
220
+ try {
221
+ const keys = Object.keys(localStorage)
222
+ keys.forEach(key => {
223
+ if (key.startsWith(CACHE_KEYS.PREFIX)) {
224
+ const item = safeJSONParse(localStorage.getItem(key))
225
+ if (item && isExpired(item.timestamp, this.ttl)) {
226
+ localStorage.removeItem(key)
227
+ pruned++
228
+ }
229
+ }
230
+ })
231
+ } catch (e) {
232
+ logger.warn('Error pruning cache:', e)
233
+ }
234
+ }
235
+
236
+ if (pruned > 0) {
237
+ logger.info(`Pruned ${pruned} expired cache items`)
238
+ }
239
+
240
+ return pruned
241
+ }
242
+ }
243
+
244
+ export default new CacheManager()
245
+
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Mixpeek Context Adapter - Constants
3
+ * @module config/constants
4
+ */
5
+
6
+ export const MIXPEEK_MODULE_NAME = 'mixpeek'
7
+ export const MIXPEEK_VERSION = '1.0.0'
8
+
9
+ // API Configuration
10
+ // Use MIXPEEK_API_ENDPOINT environment variable or default to production
11
+ export const DEFAULT_API_ENDPOINT = typeof process !== 'undefined' && process.env && process.env.MIXPEEK_API_ENDPOINT
12
+ ? process.env.MIXPEEK_API_ENDPOINT
13
+ : (typeof window !== 'undefined' && window.MIXPEEK_API_ENDPOINT
14
+ ? window.MIXPEEK_API_ENDPOINT
15
+ : 'https://api.mixpeek.com')
16
+
17
+ // Alternative endpoints
18
+ export const API_ENDPOINTS = {
19
+ PRODUCTION: 'https://api.mixpeek.com',
20
+ DEVELOPMENT: 'https://server-xb24.onrender.com',
21
+ LOCAL: 'http://localhost:8000'
22
+ }
23
+
24
+ export const DEFAULT_TIMEOUT = 250 // milliseconds
25
+ export const DEFAULT_CACHE_TTL = 300 // seconds
26
+ export const DEFAULT_RETRY_ATTEMPTS = 2
27
+ export const DEFAULT_BATCH_SIZE = 1
28
+
29
+ // API Endpoints
30
+ export const ENDPOINTS = {
31
+ COLLECTIONS: '/v1/collections',
32
+ DOCUMENTS: '/v1/collections/{collectionId}/documents',
33
+ FEATURES: '/v1/collections/{collectionId}/documents/{documentId}/features',
34
+ FEATURE_EXTRACTORS: '/v1/collections/features/extractors',
35
+ RETRIEVERS: '/v1/retrievers/debug-inference'
36
+ }
37
+
38
+ // Content Modes
39
+ export const CONTENT_MODES = {
40
+ AUTO: 'auto',
41
+ PAGE: 'page',
42
+ VIDEO: 'video',
43
+ IMAGE: 'image'
44
+ }
45
+
46
+ // Feature Extractors
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'
54
+ }
55
+
56
+ // Targeting Key Prefixes
57
+ export const TARGETING_KEYS = {
58
+ TAXONOMY: 'hb_mixpeek_taxonomy',
59
+ CATEGORY: 'hb_mixpeek_category',
60
+ NODE: 'hb_mixpeek_node',
61
+ PATH: 'hb_mixpeek_path',
62
+ SCORE: 'hb_mixpeek_score',
63
+ SAFETY: 'hb_mixpeek_safety',
64
+ KEYWORDS: 'hb_mixpeek_keywords',
65
+ EMBED: 'hb_mixpeek_embed',
66
+ SENTIMENT: 'hb_mixpeek_sentiment',
67
+ // Previous ad context
68
+ PREV_AD_CREATIVE_ID: 'hb_mixpeek_prev_creative',
69
+ PREV_AD_BIDDER: 'hb_mixpeek_prev_bidder',
70
+ PREV_AD_ADUNIT: 'hb_mixpeek_prev_adunit',
71
+ PREV_AD_CAT: 'hb_mixpeek_prev_cat'
72
+ }
73
+
74
+ // Error Codes
75
+ export const ERROR_CODES = {
76
+ INVALID_CONFIG: 'INVALID_CONFIG',
77
+ API_TIMEOUT: 'API_TIMEOUT',
78
+ API_ERROR: 'API_ERROR',
79
+ NETWORK_ERROR: 'NETWORK_ERROR',
80
+ INVALID_RESPONSE: 'INVALID_RESPONSE',
81
+ MISSING_CONTENT: 'MISSING_CONTENT',
82
+ CACHE_ERROR: 'CACHE_ERROR'
83
+ }
84
+
85
+ // Cache Configuration
86
+ export const CACHE_KEYS = {
87
+ PREFIX: 'mixpeek_ctx_',
88
+ VERSION: 'v1'
89
+ }
90
+
91
+ // Performance Thresholds
92
+ export const PERFORMANCE = {
93
+ MAX_LATENCY: 250, // ms
94
+ WARN_LATENCY: 100, // ms
95
+ MAX_CONTENT_SIZE: 50000 // characters
96
+ }
97
+
98
+ // Default Feature Extractor Configurations
99
+ export const DEFAULT_EXTRACTORS = [
100
+ {
101
+ type: FEATURE_EXTRACTORS.TAXONOMY,
102
+ enabled: true
103
+ }
104
+ ]
105
+
106
+ // Event Names
107
+ export const EVENTS = {
108
+ CONTEXT_READY: 'mixpeekContextReady',
109
+ CONTEXT_ERROR: 'mixpeekContextError',
110
+ CONTEXT_CACHED: 'mixpeekContextCached',
111
+ API_REQUEST: 'mixpeekApiRequest',
112
+ API_RESPONSE: 'mixpeekApiResponse'
113
+ }
114
+
115
+ // HTTP Headers
116
+ export const HEADERS = {
117
+ AUTHORIZATION: 'Authorization',
118
+ NAMESPACE: 'X-Namespace',
119
+ CONTENT_TYPE: 'Content-Type',
120
+ USER_AGENT: 'User-Agent'
121
+ }
122
+
123
+ // User Agent
124
+ export const USER_AGENT = `Mixpeek-Prebid-Adapter/${MIXPEEK_VERSION}`
125
+