Skip to content

Commit

Permalink
Qortex Rtd Provider : implements rate limiting options for qortex enr…
Browse files Browse the repository at this point in the history
…ichment and analytics (prebid#12372)

* include code from local branch

* newline for linter
  • Loading branch information
shilohannese authored Oct 29, 2024
1 parent 16b49d1 commit 2fdecc6
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 30 deletions.
77 changes: 48 additions & 29 deletions modules/qortexRtdProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ function onAuctionEndEvent (data, config, t) {
.then(result => {
logMessage('Qortex analytics event sent')
})
.catch(e => logWarn(e?.message))
.catch(e => logWarn(e.message))
}
}

Expand All @@ -93,7 +93,7 @@ function onAuctionEndEvent (data, config, t) {
* @returns {Promise} ortb Content object
*/
export function getContext () {
if (qortexSessionInfo.currentSiteContext === null) {
if (!qortexSessionInfo.currentSiteContext) {
const pageUrlObject = { pageUrl: qortexSessionInfo.indexData?.pageUrl ?? '' }
logMessage('Requesting new context data');
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -145,16 +145,16 @@ export function getGroupConfig () {
*/
export function sendAnalyticsEvent(eventType, subType, data) {
if (qortexSessionInfo.analyticsUrl !== null) {
if (shouldSendAnalytics()) {
if (shouldSendAnalytics(data)) {
const analtyicsEventObject = generateAnalyticsEventObject(eventType, subType, data)
logMessage('Sending qortex analytics event');
return new Promise((resolve, reject) => {
const callbacks = {
success() {
resolve();
},
error(error) {
reject(new Error(error));
error(e, x) {
reject(new Error('Returned error status code: ' + x.status));
}
}
ajax(qortexSessionInfo.analyticsUrl, callbacks, JSON.stringify(analtyicsEventObject), {contentType: 'application/json'})
Expand Down Expand Up @@ -183,7 +183,7 @@ export function generateAnalyticsEventObject(eventType, subType, data) {
}

/**
* Creates page index data for Qortex analysis
* Determines API host for Qortex
* @param qortexUrlBase api url from config or default
* @returns {string} Qortex analytics host url
*/
Expand All @@ -205,15 +205,19 @@ export function addContextToRequests (reqBidsConfig) {
if (qortexSessionInfo.currentSiteContext === null) {
logWarn('No context data received at this time');
} else {
const fragment = { site: {content: qortexSessionInfo.currentSiteContext} }
if (qortexSessionInfo.bidderArray?.length > 0) {
qortexSessionInfo.bidderArray.forEach(bidder => mergeDeep(reqBidsConfig.ortb2Fragments.bidder, {[bidder]: fragment}))
saveContextAdded(reqBidsConfig, qortexSessionInfo.bidderArray);
} else if (!qortexSessionInfo.bidderArray) {
mergeDeep(reqBidsConfig.ortb2Fragments.global, fragment);
saveContextAdded(reqBidsConfig);
if (checkPercentageOutcome(qortexSessionInfo.groupConfig?.prebidBidEnrichmentPercentage)) {
const fragment = { site: {content: qortexSessionInfo.currentSiteContext} }
if (qortexSessionInfo.bidderArray?.length > 0) {
qortexSessionInfo.bidderArray.forEach(bidder => mergeDeep(reqBidsConfig.ortb2Fragments.bidder, {[bidder]: fragment}))
saveContextAdded(reqBidsConfig);
} else if (!qortexSessionInfo.bidderArray) {
mergeDeep(reqBidsConfig.ortb2Fragments.global, fragment);
saveContextAdded(reqBidsConfig);
} else {
logWarn('Config contains an empty bidders array, unable to determine which bids to enrich');
}
} else {
logWarn('Config contains an empty bidders array, unable to determine which bids to enrich');
saveContextAdded(reqBidsConfig, true);
}
}
}
Expand Down Expand Up @@ -273,8 +277,7 @@ export function initializeBidEnrichment() {
}
})
.catch((e) => {
const errorStatus = e.message;
logWarn('Returned error status code: ' + errorStatus)
logWarn('Returned error status code: ' + e.message)
})
}
}
Expand Down Expand Up @@ -302,10 +305,13 @@ export function initializeModuleData(config) {
return qortexSessionInfo;
}

export function saveContextAdded(reqBids, bidders = null) {
export function saveContextAdded(reqBids, skipped = false) {
const id = reqBids.auctionId;
const contextBidders = bidders ?? Array.from(new Set(reqBids.adUnits.flatMap(adunit => adunit.bids.map(bid => bid.bidder))))
qortexSessionInfo.pageAnalysisData.contextAdded[id] = contextBidders;
const contextBidders = qortexSessionInfo.bidderArray ?? Array.from(new Set(reqBids.adUnits.flatMap(adunit => adunit.bids.map(bid => bid.bidder))))
qortexSessionInfo.pageAnalysisData.contextAdded[id] = {
bidders: contextBidders,
contextSkipped: skipped
};
}

export function setContextData(value) {
Expand All @@ -316,30 +322,43 @@ export function setGroupConfigData(value) {
qortexSessionInfo.groupConfig = value
}

export function getContextAddedEntry (id) {
return qortexSessionInfo?.pageAnalysisData?.contextAdded[id]
}

function generateSessionId() {
const randomInt = window.crypto.getRandomValues(new Uint32Array(1));
const currentDateTime = Math.floor(Date.now() / 1000);
return 'QX' + randomInt.toString() + 'X' + currentDateTime.toString()
}

function attachContextAnalytics (data) {
let qxData = {};
let qxDataAdded = false;
if (qortexSessionInfo?.pageAnalysisData?.contextAdded[data.auctionId]) {
qxData = qortexSessionInfo.currentSiteContext;
qxDataAdded = true;
const contextAddedEntry = getContextAddedEntry(data.auctionId);
if (contextAddedEntry) {
data.qortexContext = qortexSessionInfo.currentSiteContext ?? {};
data.qortexContextBidders = contextAddedEntry?.bidders;
data.qortexContextSkipped = contextAddedEntry?.contextSkipped;
return data;
} else {
logMessage(`Auction ${data.auctionId} did not interact with qortex bid enrichment`)
return null;
}
data.qortexData = qxData;
data.qortexDataAdded = qxDataAdded;
return data;
}

function shouldSendAnalytics() {
const analyticsPercentage = qortexSessionInfo.groupConfig?.prebidReportingPercentage ?? 0;
function checkPercentageOutcome(percentageValue) {
const analyticsPercentage = percentageValue ?? 0;
const randomInt = Math.random().toFixed(5) * 100;
return analyticsPercentage > randomInt;
}

function shouldSendAnalytics(data) {
if (data) {
return checkPercentageOutcome(qortexSessionInfo.groupConfig?.prebidReportingPercentage)
} else {
return false;
}
}

function shouldAllowBidEnrichment() {
if (qortexSessionInfo.bidEnrichmentDisabled) {
logWarn('Bid enrichment disabled at prebid config')
Expand Down
72 changes: 71 additions & 1 deletion test/spec/modules/qortexRtdProvider_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
initializeModuleData,
setGroupConfigData,
saveContextAdded,
initializeBidEnrichment
initializeBidEnrichment,
getContextAddedEntry
} from '../../../modules/qortexRtdProvider';
import {server} from '../../mocks/xhr.js';
import { cloneDeep } from 'lodash';
Expand Down Expand Up @@ -47,6 +48,13 @@ describe('qortexRtdProvider', () => {
bidders: validBidderArray
}
}
const invalidApiUrlModuleConfig = {
params: {
groupId: defaultGroupId,
apiUrl: 'test123',
bidders: validBidderArray
}
}
const emptyModuleConfig = {
params: {}
}
Expand Down Expand Up @@ -95,6 +103,7 @@ describe('qortexRtdProvider', () => {
groupId: defaultGroupId,
active: true,
prebidBidEnrichment: true,
prebidBidEnrichmentPercentage: 100,
prebidReportingPercentage: 100
}
const validGroupConfigResponse = JSON.stringify(validGroupConfigResponseObj);
Expand All @@ -107,6 +116,14 @@ describe('qortexRtdProvider', () => {
}
const inactiveGroupConfigResponse = JSON.stringify(inactiveGroupConfigResponseObj);

const noEnrichmentGroupConfigResponseObj = {
groupId: defaultGroupId,
active: true,
prebidBidEnrichment: true,
prebidBidEnrichmentPercentage: 0,
prebidReportingPercentage: 100
}

const reqBidsConfig = {
auctionId: '1234',
adUnits: [{
Expand Down Expand Up @@ -281,6 +298,16 @@ describe('qortexRtdProvider', () => {
server.requests[0].respond(200, responseHeaders, contextResponse);
})

it('will log message call callback if context data has already been collected', (done) => {
setContextData(contextResponseObj);
module.getBidRequestData(reqBidsConfig, callbackSpy);
setTimeout(() => {
expect(server.requests.length).to.be.eql(0);
expect(logMessageSpy.calledWith('Adding Content object from existing context data')).to.be.true;
done();
}, 250)
})

it('will catch and log error and fire callback', (done) => {
module.getBidRequestData(reqBidsConfig, callbackSpy);
server.requests[0].respond(404, responseHeaders, JSON.stringify({}));
Expand All @@ -301,6 +328,18 @@ describe('qortexRtdProvider', () => {
}
module.getBidRequestData(reqBidsConfig, cb);
})

it('Logs warning for network error', (done) => {
saveContextAdded(reqBidsConfig);
const testData = {auctionId: reqBidsConfig.auctionId, data: 'data'};
module.onAuctionEndEvent(testData);
server.requests[0].respond(500, responseHeaders, JSON.stringify({}));
setTimeout(() => {
expect(logWarnSpy.calledWith('Returned error status code: 500')).to.be.eql(true);
done();
}, 200)
})

it('will not request context if prebid disable toggle is true', (done) => {
initializeModuleData(bidEnrichmentDisabledModuleConfig);
const cb = function () {
Expand Down Expand Up @@ -391,6 +430,27 @@ describe('qortexRtdProvider', () => {
})

describe('addContextToRequests', () => {
let testReqBids;
beforeEach(() => {
setGroupConfigData(validGroupConfigResponseObj);
testReqBids = {
auctionId: '1234',
adUnits: [{
bids: [
{ bidder: 'qortex' }
]
}],
ortb2Fragments: {
bidder: {},
global: {}
}
}
})

afterEach(() => {
setGroupConfigData(null);
})

it('logs error if no data was retrieved from get context call', () => {
initializeModuleData(validModuleConfig);
addContextToRequests(reqBidsConfig);
Expand All @@ -400,6 +460,16 @@ describe('qortexRtdProvider', () => {
expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({});
})

it('saves context added entry with skipped flag if valid request does not meet threshold', () => {
initializeModuleData(validModuleConfig);
setContextData(contextResponseObj.content);
setGroupConfigData(noEnrichmentGroupConfigResponseObj);
addContextToRequests(reqBidsConfig);
const contextAdded = getContextAddedEntry(reqBidsConfig.auctionId);
expect(contextAdded).to.not.be.null;
expect(contextAdded.contextSkipped).to.eql(true);
})

it('adds site.content only to global ortb2 when bidders array is omitted', () => {
const omittedBidderArrayConfig = cloneDeep(validModuleConfig);
delete omittedBidderArrayConfig.params.bidders;
Expand Down

0 comments on commit 2fdecc6

Please sign in to comment.