@@ -6,10 +6,9 @@ import * as events from '../src/events.js';
66import { EVENTS } from '../src/constants.js' ;
77import { MODULE_TYPE_RTD } from '../src/activities/modules.js' ;
88
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 = { }
1312
1413/**
1514 * Init if module configuration is valid
@@ -22,11 +21,34 @@ function init (config) {
2221 return false ;
2322 } else {
2423 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 ;
2551 }
26- if ( config ?. params ?. tagConfig ) {
27- loadScriptTag ( config )
28- }
29- return true ;
3052}
3153
3254/**
@@ -35,62 +57,161 @@ function init (config) {
3557 * @param {Function } callback Called on completion
3658 */
3759function getBidRequestData ( reqBidsConfig , callback ) {
38- if ( reqBidsConfig ?. adUnits ?. length > 0 ) {
60+ if ( reqBidsConfig ?. adUnits ?. length > 0 && shouldAllowBidEnrichment ( ) ) {
3961 getContext ( )
4062 . then ( contextData => {
4163 setContextData ( contextData )
4264 addContextToRequests ( reqBidsConfig )
4365 callback ( ) ;
4466 } )
45- . catch ( ( e ) => {
46- logWarn ( e ? .message ) ;
67+ . catch ( e => {
68+ logWarn ( 'Returned error status code: ' + e . message ) ;
4769 callback ( ) ;
4870 } ) ;
4971 } 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' } ` )
5173 callback ( ) ;
5274 }
5375}
5476
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+
5591/**
5692 * determines whether to send a request to context api and does so if necessary
5793 * @returns {Promise } ortb Content object
5894 */
5995export function getContext ( ) {
60- if ( ! currentSiteContext ) {
96+ if ( qortexSessionInfo . currentSiteContext === null ) {
97+ const pageUrlObject = { pageUrl : qortexSessionInfo . indexData ?. pageUrl ?? '' }
6198 logMessage ( 'Requesting new context data' ) ;
6299 return new Promise ( ( resolve , reject ) => {
63100 const callbacks = {
64101 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+ }
66108 resolve ( result ) ;
67109 } ,
68- error ( error ) {
69- reject ( new Error ( error ) ) ;
110+ error ( e , x ) {
111+ const responseStatus = x . status ;
112+ reject ( new Error ( responseStatus ) ) ;
70113 }
71114 }
72- ajax ( requestUrl , callbacks )
115+ ajax ( qortexSessionInfo . contextUrl , callbacks , JSON . stringify ( pageUrlObject ) , { contentType : 'application/json' } )
73116 } )
74117 } else {
75118 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' ;
77197 }
78198}
79199
80200/**
81201 * Updates bidder configs with the response from Qortex context services
82202 * @param {Object } reqBidsConfig Bid request configuration object
83- * @param {string[] } bidders Bidders specified in module's configuration
84203 */
85204export function addContextToRequests ( reqBidsConfig ) {
86- if ( currentSiteContext === null ) {
205+ if ( qortexSessionInfo . currentSiteContext === null ) {
87206 logWarn ( 'No context data received at this time' ) ;
88207 } 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 ) {
93213 mergeDeep ( reqBidsConfig . ortb2Fragments . global , fragment ) ;
214+ saveContextAdded ( reqBidsConfig ) ;
94215 } else {
95216 logWarn ( 'Config contains an empty bidders array, unable to determine which bids to enrich' ) ;
96217 }
@@ -122,45 +243,119 @@ export function loadScriptTag(config) {
122243 switch ( e ?. detail ?. type ) {
123244 case 'qx-impression' :
124245 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` )
127248 return ;
128249 } else {
129- logMessage ( 'received billable event: qx-impression' )
130- impressionIds . add ( uid )
250+ logMessage ( 'Received billable event: qx-impression' )
251+ qortexSessionInfo . impressionIds . add ( uid )
131252 billableEvent . transactionId = e . detail . uid ;
132253 events . emit ( EVENTS . BILLABLE_EVENT , billableEvent ) ;
133254 break ;
134255 }
135256 default :
136- logWarn ( `received invalid billable event: ${ e . detail ?. type } ` )
257+ logWarn ( `Received invalid billable event: ${ e . detail ?. type } ` )
137258 }
138259 } )
139260
140261 loadExternalScript ( src , MODULE_TYPE_RTD , code , undefined , undefined , attr ) ;
141262}
142263
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+ }
143281/**
144282 * Helper function to set initial values when they are obtained by init
145283 * @param {Object } config module config obtained during init
146284 */
147285export 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 ;
154309}
155310
156311export 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
158352}
159353
160354export const qortexSubmodule = {
161355 name : 'qortex' ,
162356 init,
163- getBidRequestData
357+ getBidRequestData,
358+ onAuctionEndEvent
164359}
165360
166361submodule ( 'realTimeData' , qortexSubmodule ) ;
0 commit comments