Skip to content

Commit 80fbc98

Browse files
QortexRtdProvider: Supports new Qortex bid enrichment process (prebid#12173)
* creates config request step * gather page data and send POST * includes player events logic * rtd MVP * change function name * saving before methodology change * satifies coverage and information specification:wq * removes adapter * remove dependencies * adds final MVP features * fixed submodules line * use cryptography * use textcontent per circleci * spelling * Prebid config options (#7) * rearrange logic, needs a few more tests * updated and unit tests written * remove logs * limits the type and amount of text collected on a page (#8) * fix lint errors * updates config param to be opt in * update markdown * resolve circle ci issue * new branch from updated pr-stage * resolves tests after code removal * spelling and CICD error * spelling * reorder md to match github io page: --------- Co-authored-by: rrochwick <[email protected]>
1 parent 9073a02 commit 80fbc98

File tree

3 files changed

+539
-118
lines changed

3 files changed

+539
-118
lines changed

modules/qortexRtdProvider.js

+232-37
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ import * as events from '../src/events.js';
66
import { EVENTS } from '../src/constants.js';
77
import { 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
*/
3759
function 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
*/
5995
export 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
*/
85204
export 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
*/
147285
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;
154309
}
155310

156311
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
158352
}
159353

160354
export const qortexSubmodule = {
161355
name: 'qortex',
162356
init,
163-
getBidRequestData
357+
getBidRequestData,
358+
onAuctionEndEvent
164359
}
165360

166361
submodule('realTimeData', qortexSubmodule);

0 commit comments

Comments
 (0)