@@ -6,10 +6,9 @@ import * as events from '../src/events.js';
6
6
import { EVENTS } from '../src/constants.js' ;
7
7
import { MODULE_TYPE_RTD } from '../src/activities/modules.js' ;
8
8
9
- let requestUrl ;
10
- let bidderArray ;
11
- let impressionIds ;
12
- let currentSiteContext ;
9
+ const DEFAULT_API_URL = 'https://demand.qortex.ai' ;
10
+
11
+ const qortexSessionInfo = { }
13
12
14
13
/**
15
14
* Init if module configuration is valid
@@ -22,11 +21,34 @@ function init (config) {
22
21
return false ;
23
22
} else {
24
23
initializeModuleData ( config ) ;
24
+ if ( config ?. params ?. enableBidEnrichment ) {
25
+ logMessage ( 'Requesting Qortex group configuration' )
26
+ getGroupConfig ( )
27
+ . then ( groupConfig => {
28
+ logMessage ( [ 'Received response for qortex group config' , groupConfig ] )
29
+ if ( groupConfig ?. active === true && groupConfig ?. prebidBidEnrichment === true ) {
30
+ setGroupConfigData ( groupConfig ) ;
31
+ initializeBidEnrichment ( ) ;
32
+ } else {
33
+ logWarn ( 'Group config is not configured for qortex bid enrichment' )
34
+ setGroupConfigData ( groupConfig ) ;
35
+ }
36
+ } )
37
+ . catch ( ( e ) => {
38
+ const errorStatus = e . message ;
39
+ logWarn ( 'Returned error status code: ' + errorStatus ) ;
40
+ if ( errorStatus == 404 ) {
41
+ logWarn ( 'No Group Config found' ) ;
42
+ }
43
+ } ) ;
44
+ } else {
45
+ logWarn ( 'Bid Enrichment Function has been disabled in module configuration' )
46
+ }
47
+ if ( config ?. params ?. tagConfig ) {
48
+ loadScriptTag ( config )
49
+ }
50
+ return true ;
25
51
}
26
- if ( config ?. params ?. tagConfig ) {
27
- loadScriptTag ( config )
28
- }
29
- return true ;
30
52
}
31
53
32
54
/**
@@ -35,62 +57,161 @@ function init (config) {
35
57
* @param {Function } callback Called on completion
36
58
*/
37
59
function getBidRequestData ( reqBidsConfig , callback ) {
38
- if ( reqBidsConfig ?. adUnits ?. length > 0 ) {
60
+ if ( reqBidsConfig ?. adUnits ?. length > 0 && shouldAllowBidEnrichment ( ) ) {
39
61
getContext ( )
40
62
. then ( contextData => {
41
63
setContextData ( contextData )
42
64
addContextToRequests ( reqBidsConfig )
43
65
callback ( ) ;
44
66
} )
45
- . catch ( ( e ) => {
46
- logWarn ( e ? .message ) ;
67
+ . catch ( e => {
68
+ logWarn ( 'Returned error status code: ' + e . message ) ;
47
69
callback ( ) ;
48
70
} ) ;
49
71
} else {
50
- logWarn ( 'No adunits found on request bids configuration: ' + JSON . stringify ( reqBidsConfig ) )
72
+ logWarn ( 'Module function is paused due to configuration \n Module Config : ' + JSON . stringify ( reqBidsConfig ) + `\n Group Config: ${ JSON . stringify ( qortexSessionInfo . groupConfig ) ?? 'NO GROUP CONFIG' } ` )
51
73
callback ( ) ;
52
74
}
53
75
}
54
76
77
+ /**
78
+ * Processess auction end events for Qortex reporting
79
+ * @param {Object } data Auction end object
80
+ */
81
+ function onAuctionEndEvent ( data , config , t ) {
82
+ if ( shouldAllowBidEnrichment ( ) ) {
83
+ sendAnalyticsEvent ( 'AUCTION' , 'AUCTION_END' , attachContextAnalytics ( data ) )
84
+ . then ( result => {
85
+ logMessage ( 'Qortex analytics event sent' )
86
+ } )
87
+ . catch ( e => logWarn ( e ?. message ) )
88
+ }
89
+ }
90
+
55
91
/**
56
92
* determines whether to send a request to context api and does so if necessary
57
93
* @returns {Promise } ortb Content object
58
94
*/
59
95
export function getContext ( ) {
60
- if ( ! currentSiteContext ) {
96
+ if ( qortexSessionInfo . currentSiteContext === null ) {
97
+ const pageUrlObject = { pageUrl : qortexSessionInfo . indexData ?. pageUrl ?? '' }
61
98
logMessage ( 'Requesting new context data' ) ;
62
99
return new Promise ( ( resolve , reject ) => {
63
100
const callbacks = {
64
101
success ( text , data ) {
65
- const result = data . status === 200 ? JSON . parse ( data . response ) ?. content : null ;
102
+ const responseStatus = data . status ;
103
+ let result = null ;
104
+ if ( responseStatus === 200 ) {
105
+ qortexSessionInfo . pageAnalysisData . contextRetrieved = true
106
+ result = JSON . parse ( data . response ) ?. content ;
107
+ }
66
108
resolve ( result ) ;
67
109
} ,
68
- error ( error ) {
69
- reject ( new Error ( error ) ) ;
110
+ error ( e , x ) {
111
+ const responseStatus = x . status ;
112
+ reject ( new Error ( responseStatus ) ) ;
70
113
}
71
114
}
72
- ajax ( requestUrl , callbacks )
115
+ ajax ( qortexSessionInfo . contextUrl , callbacks , JSON . stringify ( pageUrlObject ) , { contentType : 'application/json' } )
73
116
} )
74
117
} else {
75
118
logMessage ( 'Adding Content object from existing context data' ) ;
76
- return new Promise ( resolve => resolve ( currentSiteContext ) ) ;
119
+ return new Promise ( ( resolve , reject ) => resolve ( qortexSessionInfo . currentSiteContext ) ) ;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Requests Qortex group configuration using group id
125
+ * @returns {Promise } Qortex group configuration
126
+ */
127
+ export function getGroupConfig ( ) {
128
+ return new Promise ( ( resolve , reject ) => {
129
+ const callbacks = {
130
+ success ( text , data ) {
131
+ const result = data . status === 200 ? JSON . parse ( data . response ) : null ;
132
+ resolve ( result ) ;
133
+ } ,
134
+ error ( e , x ) {
135
+ reject ( new Error ( x . status ) ) ;
136
+ }
137
+ }
138
+ ajax ( qortexSessionInfo . groupConfigUrl , callbacks )
139
+ } )
140
+ }
141
+
142
+ /**
143
+ * Sends analytics events to Qortex
144
+ * @returns {Promise }
145
+ */
146
+ export function sendAnalyticsEvent ( eventType , subType , data ) {
147
+ if ( qortexSessionInfo . analyticsUrl !== null ) {
148
+ if ( shouldSendAnalytics ( ) ) {
149
+ const analtyicsEventObject = generateAnalyticsEventObject ( eventType , subType , data )
150
+ logMessage ( 'Sending qortex analytics event' ) ;
151
+ return new Promise ( ( resolve , reject ) => {
152
+ const callbacks = {
153
+ success ( ) {
154
+ resolve ( ) ;
155
+ } ,
156
+ error ( error ) {
157
+ reject ( new Error ( error ) ) ;
158
+ }
159
+ }
160
+ ajax ( qortexSessionInfo . analyticsUrl , callbacks , JSON . stringify ( analtyicsEventObject ) , { contentType : 'application/json' } )
161
+ } )
162
+ } else {
163
+ return new Promise ( ( resolve , reject ) => reject ( new Error ( 'Current request did not meet analytics percentage threshold, cancelling sending event' ) ) ) ;
164
+ }
165
+ } else {
166
+ return new Promise ( ( resolve , reject ) => reject ( new Error ( 'Analytics host not initialized' ) ) ) ;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Creates analytics object for Qortex
172
+ * @returns {Object } analytics object
173
+ */
174
+ export function generateAnalyticsEventObject ( eventType , subType , data ) {
175
+ return {
176
+ sessionId : qortexSessionInfo . sessionId ,
177
+ groupId : qortexSessionInfo . groupId ,
178
+ eventType : eventType ,
179
+ subType : subType ,
180
+ eventOriginSource : 'RTD' ,
181
+ data : data
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Creates page index data for Qortex analysis
187
+ * @param qortexUrlBase api url from config or default
188
+ * @returns {string } Qortex analytics host url
189
+ */
190
+ export function generateAnalyticsHostUrl ( qortexUrlBase ) {
191
+ if ( qortexUrlBase === DEFAULT_API_URL ) {
192
+ return 'https://events.qortex.ai/api/v1/player-event' ;
193
+ } else if ( qortexUrlBase . includes ( 'stg-demand' ) ) {
194
+ return 'https://stg-events.qortex.ai/api/v1/player-event' ;
195
+ } else {
196
+ return 'https://dev-events.qortex.ai/api/v1/player-event' ;
77
197
}
78
198
}
79
199
80
200
/**
81
201
* Updates bidder configs with the response from Qortex context services
82
202
* @param {Object } reqBidsConfig Bid request configuration object
83
- * @param {string[] } bidders Bidders specified in module's configuration
84
203
*/
85
204
export function addContextToRequests ( reqBidsConfig ) {
86
- if ( currentSiteContext === null ) {
205
+ if ( qortexSessionInfo . currentSiteContext === null ) {
87
206
logWarn ( 'No context data received at this time' ) ;
88
207
} else {
89
- const fragment = { site : { content : currentSiteContext } }
90
- if ( bidderArray ?. length > 0 ) {
91
- bidderArray . forEach ( bidder => mergeDeep ( reqBidsConfig . ortb2Fragments . bidder , { [ bidder ] : fragment } ) )
92
- } else if ( ! bidderArray ) {
208
+ const fragment = { site : { content : qortexSessionInfo . currentSiteContext } }
209
+ if ( qortexSessionInfo . bidderArray ?. length > 0 ) {
210
+ qortexSessionInfo . bidderArray . forEach ( bidder => mergeDeep ( reqBidsConfig . ortb2Fragments . bidder , { [ bidder ] : fragment } ) )
211
+ saveContextAdded ( reqBidsConfig , qortexSessionInfo . bidderArray ) ;
212
+ } else if ( ! qortexSessionInfo . bidderArray ) {
93
213
mergeDeep ( reqBidsConfig . ortb2Fragments . global , fragment ) ;
214
+ saveContextAdded ( reqBidsConfig ) ;
94
215
} else {
95
216
logWarn ( 'Config contains an empty bidders array, unable to determine which bids to enrich' ) ;
96
217
}
@@ -122,45 +243,119 @@ export function loadScriptTag(config) {
122
243
switch ( e ?. detail ?. type ) {
123
244
case 'qx-impression' :
124
245
const { uid} = e . detail ;
125
- if ( ! uid || impressionIds . has ( uid ) ) {
126
- logWarn ( `received invalid billable event due to ${ ! uid ? 'missing' : 'duplicate' } uid: qx-impression` )
246
+ if ( ! uid || qortexSessionInfo . impressionIds . has ( uid ) ) {
247
+ logWarn ( `Received invalid billable event due to ${ ! uid ? 'missing' : 'duplicate' } uid: qx-impression` )
127
248
return ;
128
249
} else {
129
- logMessage ( 'received billable event: qx-impression' )
130
- impressionIds . add ( uid )
250
+ logMessage ( 'Received billable event: qx-impression' )
251
+ qortexSessionInfo . impressionIds . add ( uid )
131
252
billableEvent . transactionId = e . detail . uid ;
132
253
events . emit ( EVENTS . BILLABLE_EVENT , billableEvent ) ;
133
254
break ;
134
255
}
135
256
default :
136
- logWarn ( `received invalid billable event: ${ e . detail ?. type } ` )
257
+ logWarn ( `Received invalid billable event: ${ e . detail ?. type } ` )
137
258
}
138
259
} )
139
260
140
261
loadExternalScript ( src , MODULE_TYPE_RTD , code , undefined , undefined , attr ) ;
141
262
}
142
263
264
+ export function initializeBidEnrichment ( ) {
265
+ if ( shouldAllowBidEnrichment ( ) ) {
266
+ getContext ( )
267
+ . then ( contextData => {
268
+ if ( qortexSessionInfo . pageAnalysisData . contextRetrieved ) {
269
+ logMessage ( 'Contextual record Received from Qortex API' )
270
+ setContextData ( contextData )
271
+ } else {
272
+ logWarn ( 'Contexual record is not yet complete at this time' )
273
+ }
274
+ } )
275
+ . catch ( ( e ) => {
276
+ const errorStatus = e . message ;
277
+ logWarn ( 'Returned error status code: ' + errorStatus )
278
+ } )
279
+ }
280
+ }
143
281
/**
144
282
* Helper function to set initial values when they are obtained by init
145
283
* @param {Object } config module config obtained during init
146
284
*/
147
285
export function initializeModuleData ( config ) {
148
- const DEFAULT_API_URL = 'https://demand.qortex.ai' ;
149
- const { apiUrl, groupId, bidders} = config . params ;
150
- requestUrl = `${ apiUrl || DEFAULT_API_URL } /api/v1/analyze/${ groupId } /prebid` ;
151
- bidderArray = bidders ;
152
- impressionIds = new Set ( ) ;
153
- currentSiteContext = null ;
286
+ const { apiUrl, groupId, bidders, enableBidEnrichment} = config . params ;
287
+ const qortexUrlBase = apiUrl || DEFAULT_API_URL ;
288
+ const windowUrl = window . top . location . host ;
289
+ qortexSessionInfo . bidEnrichmentDisabled = enableBidEnrichment !== null ? ! enableBidEnrichment : true ;
290
+ qortexSessionInfo . bidderArray = bidders ;
291
+ qortexSessionInfo . impressionIds = new Set ( ) ;
292
+ qortexSessionInfo . currentSiteContext = null ;
293
+ qortexSessionInfo . pageAnalysisData = {
294
+ contextRetrieved : false ,
295
+ contextAdded : { }
296
+ } ;
297
+ qortexSessionInfo . sessionId = generateSessionId ( ) ;
298
+ qortexSessionInfo . groupId = groupId ;
299
+ qortexSessionInfo . groupConfigUrl = `${ qortexUrlBase } /api/v1/prebid/group/configs/${ groupId } /${ windowUrl } ` ;
300
+ qortexSessionInfo . contextUrl = `${ qortexUrlBase } /api/v1/prebid/${ groupId } /page/lookup` ;
301
+ qortexSessionInfo . analyticsUrl = generateAnalyticsHostUrl ( qortexUrlBase ) ;
302
+ return qortexSessionInfo ;
303
+ }
304
+
305
+ export function saveContextAdded ( reqBids , bidders = null ) {
306
+ const id = reqBids . auctionId ;
307
+ const contextBidders = bidders ?? Array . from ( new Set ( reqBids . adUnits . flatMap ( adunit => adunit . bids . map ( bid => bid . bidder ) ) ) )
308
+ qortexSessionInfo . pageAnalysisData . contextAdded [ id ] = contextBidders ;
154
309
}
155
310
156
311
export function setContextData ( value ) {
157
- currentSiteContext = value
312
+ qortexSessionInfo . currentSiteContext = value
313
+ }
314
+
315
+ export function setGroupConfigData ( value ) {
316
+ qortexSessionInfo . groupConfig = value
317
+ }
318
+
319
+ function generateSessionId ( ) {
320
+ const randomInt = window . crypto . getRandomValues ( new Uint32Array ( 1 ) ) ;
321
+ const currentDateTime = Math . floor ( Date . now ( ) / 1000 ) ;
322
+ return 'QX' + randomInt . toString ( ) + 'X' + currentDateTime . toString ( )
323
+ }
324
+
325
+ function attachContextAnalytics ( data ) {
326
+ let qxData = { } ;
327
+ let qxDataAdded = false ;
328
+ if ( qortexSessionInfo ?. pageAnalysisData ?. contextAdded [ data . auctionId ] ) {
329
+ qxData = qortexSessionInfo . currentSiteContext ;
330
+ qxDataAdded = true ;
331
+ }
332
+ data . qortexData = qxData ;
333
+ data . qortexDataAdded = qxDataAdded ;
334
+ return data ;
335
+ }
336
+
337
+ function shouldSendAnalytics ( ) {
338
+ const analyticsPercentage = qortexSessionInfo . groupConfig ?. prebidReportingPercentage ?? 0 ;
339
+ const randomInt = Math . random ( ) . toFixed ( 5 ) * 100 ;
340
+ return analyticsPercentage > randomInt ;
341
+ }
342
+
343
+ function shouldAllowBidEnrichment ( ) {
344
+ if ( qortexSessionInfo . bidEnrichmentDisabled ) {
345
+ logWarn ( 'Bid enrichment disabled at prebid config' )
346
+ return false ;
347
+ } else if ( ! qortexSessionInfo . groupConfig ?. prebidBidEnrichment ) {
348
+ logWarn ( 'Bid enrichment disabled at group config' )
349
+ return false ;
350
+ }
351
+ return true
158
352
}
159
353
160
354
export const qortexSubmodule = {
161
355
name : 'qortex' ,
162
356
init,
163
- getBidRequestData
357
+ getBidRequestData,
358
+ onAuctionEndEvent
164
359
}
165
360
166
361
submodule ( 'realTimeData' , qortexSubmodule ) ;
0 commit comments