diff --git a/modules/qortexRtdProvider.js b/modules/qortexRtdProvider.js index c5f9226ebe8..744d44bef60 100644 --- a/modules/qortexRtdProvider.js +++ b/modules/qortexRtdProvider.js @@ -20,32 +20,34 @@ function init (config) { return false; } else { initializeModuleData(config); - getGroupConfig() - .then(groupConfig => { - logMessage(['Recieved response for qortex group config', groupConfig]) - if (groupConfig?.active === true && groupConfig?.prebidBidEnrichment === true) { - setGroupConfigData(groupConfig); - } else { - logWarn('Group config is not configured for qortex RTD module, module functions will be paused') - setGroupConfigData(groupConfig); - } - }) - .catch((e) => { - logWarn(e); - }); - - initiatePageAnalysis() - .then(successMessage => { - logMessage(successMessage) - }) - .catch((e) => { - logWarn(e?.message); - }); - } - if (config?.params?.tagConfig) { - loadScriptTag(config) + if (!config?.params?.disableBidEnrichment) { + logMessage('Requesting Qortex group configuration') + getGroupConfig() + .then(groupConfig => { + logMessage(['Recieved response for qortex group config', groupConfig]) + if (groupConfig?.active === true && groupConfig?.prebidBidEnrichment === true) { + setGroupConfigData(groupConfig); + initializeBidEnrichment(); + } else { + logWarn('Group config is not configured for qortex bid enrichment') + setGroupConfigData(groupConfig); + } + }) + .catch((e) => { + const errorStatus = e.message; + logWarn('Returned error status code: ' + errorStatus); + if (errorStatus == 404) { + logWarn('No Group Config found'); + } + }); + } else { + logWarn('Bid Enrichment Function has been disabled in module configuration') + } + if (config?.params?.tagConfig) { + loadScriptTag(config) + } + return true; } - return true; } /** @@ -54,15 +56,15 @@ function init (config) { * @param {Function} callback Called on completion */ function getBidRequestData (reqBidsConfig, callback) { - if (reqBidsConfig?.adUnits?.length > 0 && qortexSessionInfo.groupConfig?.prebidBidEnrichment === true) { + if (reqBidsConfig?.adUnits?.length > 0 && shouldAllowBidEnrichment()) { getContext() .then(contextData => { setContextData(contextData) addContextToRequests(reqBidsConfig) callback(); }) - .catch((e) => { - logWarn(e?.message); + .catch(e => { + logWarn('Returned error status code: ' + e.message); callback(); }); } else { @@ -76,7 +78,7 @@ function getBidRequestData (reqBidsConfig, callback) { * @param {Object} data Auction end object */ function onAuctionEndEvent (data, config, t) { - if (qortexSessionInfo?.groupConfig?.prebidBidEnrichment === true) { + if (shouldAllowBidEnrichment()) { sendAnalyticsEvent('AUCTION', 'AUCTION_END', attachContextAnalytics(data)) .then(result => { logMessage('Qortex anyalitics event sent') @@ -96,11 +98,20 @@ export function getContext () { return new Promise((resolve, reject) => { const callbacks = { success(text, data) { - const result = data.status === 200 ? JSON.parse(data.response)?.content : null; + const responseStatus = data.status; + let result; + if (responseStatus === 200) { + qortexSessionInfo.pageAnalysisData.contextRetrieved = true + result = JSON.parse(data.response)?.content; + } else if (responseStatus === 202) { + qortexSessionInfo.pageAnalysisData.analysisInProgress = true; + result = null; + } resolve(result); }, - error(error) { - reject(new Error(error)); + error(e, x) { + const responseStatus = x.status; + reject(new Error(responseStatus)); } } ajax(qortexSessionInfo.contextUrl, callbacks, JSON.stringify(pageUrlObject), {contentType: 'application/json'}) @@ -116,15 +127,14 @@ export function getContext () { * @returns {Promise} Qortex group configuration */ export function getGroupConfig () { - logMessage('Requesting group config'); return new Promise((resolve, reject) => { const callbacks = { success(text, data) { const result = data.status === 200 ? JSON.parse(data.response) : null; resolve(result); }, - error(error) { - reject(new Error(error)); + error(e, x) { + reject(new Error(x.status)); } } ajax(qortexSessionInfo.groupConfigUrl, callbacks) @@ -140,13 +150,19 @@ export function initiatePageAnalysis () { logMessage('Sending page data for context analysis'); return new Promise((resolve, reject) => { const callbacks = { - success() { - qortexSessionInfo.pageAnalysisdata.requestSuccessful = true; - resolve('Successfully initiated Qortex page analysis'); + success(text, data) { + const responseStatus = data.status; + let resultMessage; + if (responseStatus === 201) { + qortexSessionInfo.pageAnalysisData.indexRequested = true; + resultMessage = 'Successfully initiated Qortex page analysis'; + } else { + resultMessage = 'No index record created at this time' + } + resolve(resultMessage); }, - error(error) { - qortexSessionInfo.pageAnalysisdata.requestSuccessful = false; - reject(new Error(error)); + error(e, x) { + reject(new Error(x.status)); } } ajax(qortexSessionInfo.pageAnalyisUrl, callbacks, JSON.stringify(qortexSessionInfo.indexData), {contentType: 'application/json'}) @@ -276,20 +292,49 @@ export function loadScriptTag(config) { loadExternalScript(src, code, undefined, undefined, attr); } +export function initializeBidEnrichment() { + if (shouldAllowBidEnrichment()) { + getContext() + .then(contextData => { + if (qortexSessionInfo.pageAnalysisData.contextRetrieved) { + logMessage('Contextual record recieved from Qortex API') + setContextData(contextData) + } else { + logWarn('Contexual record is not yet complete at this time') + } + }) + .catch(e => { + const errorStatus = e.message; + logWarn('Returned error status code: ' + errorStatus); + if (errorStatus == 404) { + initiatePageAnalysis() + .then(message => { + logMessage(message) + }) + .catch(e => { + logWarn(e); + }) + } + }); + } +} + /** * Helper function to set initial values when they are obtained by init * @param {Object} config module config obtained during init */ export function initializeModuleData(config) { - const {apiUrl, groupId, bidders} = config.params; + const {apiUrl, groupId, bidders, disableBidEnrichment} = config.params; const qortexUrlBase = apiUrl || DEFAULT_API_URL; const windowUrl = window.top.location.host; + qortexSessionInfo.bidEnrichmentDisabled = disableBidEnrichment !== null ? disableBidEnrichment : false; qortexSessionInfo.bidderArray = bidders; qortexSessionInfo.impressionIds = new Set(); qortexSessionInfo.currentSiteContext = null; - qortexSessionInfo.pageAnalysisdata = { - requestSuccessful: null, - analysisGenerated: false, + qortexSessionInfo.pageAnalysisData = { + analysisInProgress: false, + indexRequested: false, + contextRetrieved: false, contextAdded: {} }; qortexSessionInfo.sessionId = generateSessionId(); @@ -304,7 +349,7 @@ export function initializeModuleData(config) { export function saveContextAdded(reqBids, bidders = null) { 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; + qortexSessionInfo.pageAnalysisData.contextAdded[id] = contextBidders; } export function setContextData(value) { @@ -338,7 +383,7 @@ function generateSessionId() { function attachContextAnalytics (data) { let qxData = {}; let qxDataAdded = false; - if (qortexSessionInfo?.pageAnalysisdata?.contextAdded[data.auctionId]) { + if (qortexSessionInfo?.pageAnalysisData?.contextAdded[data.auctionId]) { qxData = qortexSessionInfo.currentSiteContext; qxDataAdded = true; } @@ -353,6 +398,17 @@ function shouldSendAnalytics() { return analyticsPercentage > randomInt; } +function shouldAllowBidEnrichment() { + if (qortexSessionInfo.bidEnrichmentDisabled) { + logWarn('Bid enrichment disabled at prebid config') + return false; + } else if (!qortexSessionInfo.groupConfig?.prebidBidEnrichment) { + logWarn('Bid enrichment disabled at group config') + return false; + } + return true +} + export const qortexSubmodule = { name: 'qortex', init, diff --git a/modules/qortexRtdProvider.md b/modules/qortexRtdProvider.md index 9e2ae3d84eb..a499f35d6c3 100644 --- a/modules/qortexRtdProvider.md +++ b/modules/qortexRtdProvider.md @@ -40,6 +40,7 @@ pbjs.setConfig({ params: { groupId: 'ABC123', //required bidders: ['qortex', 'adapter2'], //optional (see below) + disableBidEnrichment: false, //optional (see below) tagConfig: { // optional, please reach out to your account manager for configuration reccommendation videoContainer: 'string', htmlContainer: 'string', @@ -66,4 +67,7 @@ pbjs.setConfig({ #### `tagConfig` - optional - This optional parameter is an object containing the config settings that could be usedto initialize the Qortex integration on your page. A preconfigured object for this step will be provided to you by the Qortex team. -- If this parameter is not present, the Qortex integration can still be configured and loaded manually on your page outside of prebid. The RTD module will continue to initialize and operate as normal. \ No newline at end of file +- If this parameter is not present, the Qortex integration can still be configured and loaded manually on your page outside of prebid. The RTD module will continue to initialize and operate as normal. + +#### `disableBidEnrichment` - optional +- This optional parameter allows a publisher to opt out of the RTD module from using our API to enrich bids with first party data for contextuality \ No newline at end of file diff --git a/test/spec/modules/qortexRtdProvider_spec.js b/test/spec/modules/qortexRtdProvider_spec.js index 0622cb6b691..2c7fb85a0d6 100644 --- a/test/spec/modules/qortexRtdProvider_spec.js +++ b/test/spec/modules/qortexRtdProvider_spec.js @@ -14,7 +14,8 @@ import { loadScriptTag, initializeModuleData, setGroupConfigData, - saveContextAdded + saveContextAdded, + initializeBidEnrichment } from '../../../modules/qortexRtdProvider'; import {server} from '../../mocks/xhr.js'; import { cloneDeep } from 'lodash'; @@ -39,6 +40,14 @@ describe('qortexRtdProvider', () => { bidders: validBidderArray } } + const bidEnrichmentDisabledModuleConfig = { + params: { + groupId: defaultGroupId, + apiUrl: defaultApiHost, + bidders: validBidderArray, + disableBidEnrichment: true + } + } const emptyModuleConfig = { params: {} } @@ -128,17 +137,40 @@ describe('qortexRtdProvider', () => { describe('init', () => { it('returns true for valid config object', (done) => { const result = module.init(validModuleConfig); - expect(server.requests.length).to.be.eql(2) + expect(server.requests.length).to.be.eql(1) const groupConfigReq = server.requests[0]; - const pageAnalysisReq = server.requests[1]; groupConfigReq.respond(200, responseHeaders, validGroupConfigResponse); - pageAnalysisReq.respond(200, responseHeaders, JSON.stringify({})); setTimeout(() => { expect(result).to.be.true; done() }, 500) }) + it('logs warning when group config does not pass setup conditions', (done) => { + const result = module.init(validModuleConfig); + expect(server.requests.length).to.be.eql(1) + const groupConfigReq = server.requests[0]; + groupConfigReq.respond(200, responseHeaders, inactiveGroupConfigResponse); + setTimeout(() => { + expect(logWarnSpy.calledWith('Group config is not configured for qortex bid enrichment')).to.be.true; + done() + }, 500) + }) + + it('logs warning when group config request errors', (done) => { + const result = module.init(validModuleConfig); + server.requests[0].respond(404, responseHeaders, inactiveGroupConfigResponse); + setTimeout(() => { + expect(logWarnSpy.calledWith('No Group Config found')).to.be.true; + done() + }, 500) + }) + + it('will not initialize bid enrichment if it is disabled', () => { + module.init(bidEnrichmentDisabledModuleConfig); + expect(logWarnSpy.calledWith('Bid Enrichment Function has been disabled in module configuration')).to.be.true; + }) + it('returns false and logs error for missing groupId', () => { expect(module.init(emptyModuleConfig)).to.be.false; expect(logWarnSpy.calledOnce).to.be.true; @@ -224,7 +256,9 @@ describe('qortexRtdProvider', () => { beforeEach(() => { initializeModuleData(validModuleConfig); + setGroupConfigData(validGroupConfigResponseObj); callbackSpy = sinon.spy(); + server.reset(); }) afterEach(() => { @@ -249,13 +283,34 @@ describe('qortexRtdProvider', () => { }) it('will catch and log error and fire callback', (done) => { - const a = sinon.stub(ajax, 'ajax').throws(new Error('test')); + module.getBidRequestData(reqBidsConfig, callbackSpy); + server.requests[0].respond(404, responseHeaders, JSON.stringify({})); + setTimeout(() => { + expect(logWarnSpy.calledWith('Returned error status code: 404')).to.be.eql(true); + expect(callbackSpy.calledOnce).to.be.true; + done(); + }, 250) + }) + + it('will not request context if group config toggle is false', (done) => { + setGroupConfigData(inactiveGroupConfigResponseObj); const cb = function () { - expect(logWarnSpy.calledWith('test')).to.be.eql(true); + expect(server.requests.length).to.be.eql(0); + expect(logWarnSpy.called).to.be.true; + expect(logWarnSpy.calledWith('Bid enrichment disabled at group config')).to.be.true; + done(); + } + module.getBidRequestData(reqBidsConfig, cb); + }) + it('will not request context if prebid disable toggle is true', (done) => { + initializeModuleData(bidEnrichmentDisabledModuleConfig); + const cb = function () { + expect(server.requests.length).to.be.eql(0); + expect(logWarnSpy.called).to.be.true; + expect(logWarnSpy.calledWith('Bid enrichment disabled at prebid config')).to.be.true; done(); } module.getBidRequestData(reqBidsConfig, cb); - a.restore(); }) }) @@ -325,9 +380,8 @@ describe('qortexRtdProvider', () => { }) it('returns null when necessary', (done) => { - const nullContentResponse = { content: null } const ctx = getContext() - server.requests[0].respond(200, responseHeaders, JSON.stringify(nullContentResponse)) + server.requests[0].respond(202, responseHeaders, JSON.stringify({})) ctx.then(response => { expect(response).to.be.null; expect(server.requests.length).to.be.eql(1); @@ -446,6 +500,9 @@ describe('qortexRtdProvider', () => { afterEach(() => { initializeModuleData(emptyModuleConfig); + setGroupConfigData(null); + setContextData(null); + server.reset(); }) it('returns a promise', () => { @@ -467,4 +524,64 @@ describe('qortexRtdProvider', () => { }) }) }) + + describe('initializeBidEnrichment', () => { + beforeEach(() => { + initializeModuleData(validModuleConfig); + setGroupConfigData(validGroupConfigResponseObj); + setContextData(null); + server.reset(); + }) + + afterEach(() => { + initializeModuleData(emptyModuleConfig); + setGroupConfigData(null); + setContextData(null); + server.reset(); + }) + + it('sets context data if applicable', (done) => { + initializeBidEnrichment(); + server.requests[0].respond(200, responseHeaders, contextResponse); + setTimeout(() => { + expect(logMessageSpy.calledWith('Contextual record recieved from Qortex API')).to.be.true; + done() + }, 250) + }) + + it('logs page analysis response information if initiated', (done) => { + initializeBidEnrichment(); + server.requests[0].respond(404, responseHeaders, JSON.stringify({})); + setTimeout(() => { + server.requests[1].respond(201, responseHeaders, JSON.stringify({})); + setTimeout(() => { + expect(logMessageSpy.calledWith('Sending page data for context analysis')).to.be.true; + expect(logMessageSpy.calledWith('Successfully initiated Qortex page analysis')).to.be.true; + done(); + }, 400) + }, 250) + }) + + it('logs page analysis response information if applicable', (done) => { + initializeBidEnrichment(); + server.requests[0].respond(404, responseHeaders, JSON.stringify({})); + setTimeout(() => { + server.requests[1].respond(201, responseHeaders, JSON.stringify({})); + setTimeout(() => { + expect(logMessageSpy.calledWith('Sending page data for context analysis')).to.be.true; + expect(logMessageSpy.calledWith('Successfully initiated Qortex page analysis')).to.be.true; + done(); + }, 400) + }, 250) + }) + + it('logs page analysis response if no record is made', (done) => { + initializeBidEnrichment(); + server.requests[0].respond(202, responseHeaders, JSON.stringify({})); + setTimeout(() => { + expect(logWarnSpy.calledWith('Contexual record is not yet complete at this time')).to.be.true; + done(); + }, 250) + }) + }) })