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