Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QortexRtdProvider: Supports new Qortex bid enrichment process #12173

Merged
merged 31 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bfeff50
creates config request step
shilohannese Aug 6, 2024
be9a8b7
gather page data and send POST
shilohannese Aug 8, 2024
4282e8d
includes player events logic
shilohannese Aug 19, 2024
40f6a05
rtd MVP
shilohannese Aug 21, 2024
6c1aac2
change function name
shilohannese Aug 21, 2024
d9f7a0c
saving before methodology change
shilohannese Aug 23, 2024
908460f
satifies coverage and information specification:wq
shilohannese Aug 26, 2024
b66c652
removes adapter
shilohannese Aug 26, 2024
eae4915
remove dependencies
shilohannese Aug 26, 2024
a1afab6
adds final MVP features
shilohannese Aug 27, 2024
37d9876
Merge remote-tracking branch 'origin/master' into prebid-pr-stage
shilohannese Aug 27, 2024
9d9b710
fixed submodules line
shilohannese Aug 27, 2024
8ff9ec4
use cryptography
shilohannese Aug 27, 2024
e5e5169
use textcontent per circleci
shilohannese Aug 27, 2024
df3c59f
spelling
shilohannese Aug 27, 2024
7e78f77
Prebid config options (#7)
shilohannese Sep 7, 2024
50d31e6
limits the type and amount of text collected on a page (#8)
shilohannese Sep 11, 2024
9ff3e38
fix lint errors
shilohannese Sep 11, 2024
b6a0d56
updates config param to be opt in
shilohannese Sep 25, 2024
223fd3d
update markdown
shilohannese Sep 25, 2024
93d36c3
Merge pull request #9 from firecatapult/qxd-4991-update-opt-in-flag
rrochwick Sep 26, 2024
98b526a
resolve circle ci issue
shilohannese Oct 9, 2024
dafaae9
Merge pull request #10 from firecatapult/qxd-5020-param-fix
rrochwick Oct 9, 2024
c87ba5f
Merge remote-tracking branch 'origin' into pr-sync-master
shilohannese Oct 22, 2024
efac0cb
Merge pull request #13 from firecatapult/pr-sync-master
rrochwick Oct 22, 2024
10c7b20
new branch from updated pr-stage
shilohannese Oct 22, 2024
ccb5bd7
resolves tests after code removal
shilohannese Oct 22, 2024
c1264f7
Merge pull request #14 from firecatapult/qxd-5006-remove-content-anal…
rrochwick Oct 22, 2024
8995b86
spelling and CICD error
shilohannese Oct 24, 2024
0e05060
spelling
shilohannese Oct 24, 2024
3d68641
reorder md to match github io page:
shilohannese Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 225 additions & 27 deletions modules/qortexRtdProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
import * as events from '../src/events.js';
import { EVENTS } from '../src/constants.js';

let requestUrl;
let bidderArray;
let impressionIds;
let currentSiteContext;
const DEFAULT_API_URL = 'https://demand.qortex.ai';

const qortexSessionInfo = {}

/**
* Init if module configuration is valid
Expand All @@ -21,6 +20,27 @@
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)
Expand All @@ -34,7 +54,7 @@
* @param {Function} callback Called on completion
*/
function getBidRequestData (reqBidsConfig, callback) {
if (reqBidsConfig?.adUnits?.length > 0) {
if (reqBidsConfig?.adUnits?.length > 0 && qortexSessionInfo.groupConfig?.prebidBidEnrichment === true) {
getContext()
.then(contextData => {
setContextData(contextData)
Expand All @@ -46,17 +66,32 @@
callback();
});
} else {
logWarn('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfig))
logWarn('Module function is paused due to configuration \n Module Config: ' + JSON.stringify(reqBidsConfig) + `\n Group Config: ${JSON.stringify(qortexSessionInfo.groupConfig) ?? 'NO GROUP CONFIG'}`)
callback();
}
}

/**
* Processess auction end events for Qortex reporting
* @param {Object} data Auction end object
*/
function onAuctionEndEvent (data, config, t) {
if (qortexSessionInfo?.groupConfig?.prebidBidEnrichment === true) {
sendAnalyticsEvent('AUCTION', 'AUCTION_END', attachContextAnalytics(data))
.then(result => {
logMessage('Qortex anyalitics event sent')
shilohannese marked this conversation as resolved.
Show resolved Hide resolved
})
.catch(e => logWarn(e?.message))
}
}

/**
* determines whether to send a request to context api and does so if necessary
* @returns {Promise} ortb Content object
*/
export function getContext () {
if (!currentSiteContext) {
if (qortexSessionInfo.currentSiteContext === null) {
const pageUrlObject = { pageUrl: qortexSessionInfo.indexData?.pageUrl ?? '' }
logMessage('Requesting new context data');
return new Promise((resolve, reject) => {
const callbacks = {
Expand All @@ -68,11 +103,111 @@
reject(new Error(error));
}
}
ajax(requestUrl, callbacks)
ajax(qortexSessionInfo.contextUrl, callbacks, JSON.stringify(pageUrlObject), {contentType: 'application/json'})
})
} else {
logMessage('Adding Content object from existing context data');
return new Promise(resolve => resolve(currentSiteContext));
return new Promise((resolve, reject) => resolve(qortexSessionInfo.currentSiteContext));
}
}

/**
* Requests Qortex group configuration using group id
* @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));
}
}
ajax(qortexSessionInfo.groupConfigUrl, callbacks)
})
}

/**
* Initiates page analysis from Qortex
* @returns {Promise}
*/
export function initiatePageAnalysis () {
qortexSessionInfo.indexData = generateIndexData();
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');
},
error(error) {
qortexSessionInfo.pageAnalysisdata.requestSuccessful = false;
reject(new Error(error));
}
}
ajax(qortexSessionInfo.pageAnalyisUrl, callbacks, JSON.stringify(qortexSessionInfo.indexData), {contentType: 'application/json'})
})
}

/**
* Sends analytics events to Qortex
* @returns {Promise}
*/
export function sendAnalyticsEvent(eventType, subType, data) {
if (qortexSessionInfo.analyticsUrl !== null) {
if (shouldSendAnalytics()) {
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));
}
}
ajax(qortexSessionInfo.analyticsUrl, callbacks, JSON.stringify(analtyicsEventObject), {contentType: 'application/json'})
})
} else {
return new Promise((resolve, reject) => reject(new Error('Current request did not meet analytics percentage threshold, cancelling sending event')));
}
} else {
return new Promise((resolve, reject) => reject(new Error('Analytics host not initialized')));
}
}

/**
* Creates analytics object for Qortex
* @returns {Object} analytics object
*/
export function generateAnalyticsEventObject(eventType, subType, data) {
return {
sessionId: qortexSessionInfo.sessionId,
groupId: qortexSessionInfo.groupId,
eventType: eventType,
subType: subType,
eventOriginSource: 'RTD',
data: data
}
}

/**
* Creates page index data for Qortex analysis
* @param qortexUrlBase api url from config or default
* @returns {string} Qortex analytics host url
*/
export function generateAnalyticsHostUrl(qortexUrlBase) {
if (qortexUrlBase === DEFAULT_API_URL) {
return 'https://events.qortex.ai/api/v1/player-event';
} else if (qortexUrlBase.includes('stg-demand')) {
return 'https://stg-events.qortex.ai/api/v1/player-event';
} else {
return 'https://dev-events.qortex.ai/api/v1/player-event';
}
}

Expand All @@ -82,14 +217,16 @@
* @param {string[]} bidders Bidders specified in module's configuration
*/
export function addContextToRequests (reqBidsConfig) {
if (currentSiteContext === null) {
logWarn('No context data received at this time');
if (qortexSessionInfo.currentSiteContext === null) {
logWarn('No context data recieved at this time');
} else {
const fragment = { site: {content: currentSiteContext} }
if (bidderArray?.length > 0) {
bidderArray.forEach(bidder => mergeDeep(reqBidsConfig.ortb2Fragments.bidder, {[bidder]: fragment}))
} else if (!bidderArray) {
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);
} else {
logWarn('Config contains an empty bidders array, unable to determine which bids to enrich');
}
Expand Down Expand Up @@ -121,18 +258,18 @@
switch (e?.detail?.type) {
case 'qx-impression':
const {uid} = e.detail;
if (!uid || impressionIds.has(uid)) {
logWarn(`received invalid billable event due to ${!uid ? 'missing' : 'duplicate'} uid: qx-impression`)
if (!uid || qortexSessionInfo.impressionIds.has(uid)) {
logWarn(`Recieved invalid billable event due to ${!uid ? 'missing' : 'duplicate'} uid: qx-impression`)
shilohannese marked this conversation as resolved.
Show resolved Hide resolved
return;
} else {
logMessage('received billable event: qx-impression')
impressionIds.add(uid)
logMessage('Recieved billable event: qx-impression')
qortexSessionInfo.impressionIds.add(uid)
billableEvent.transactionId = e.detail.uid;
events.emit(EVENTS.BILLABLE_EVENT, billableEvent);
break;
}
default:
logWarn(`received invalid billable event: ${e.detail?.type}`)
logWarn(`Recieved invalid billable event: ${e.detail?.type}`)
}
})

Expand All @@ -144,22 +281,83 @@
* @param {Object} config module config obtained during init
*/
export function initializeModuleData(config) {
const DEFAULT_API_URL = 'https://demand.qortex.ai';
const {apiUrl, groupId, bidders} = config.params;
requestUrl = `${apiUrl || DEFAULT_API_URL}/api/v1/analyze/${groupId}/prebid`;
bidderArray = bidders;
impressionIds = new Set();
currentSiteContext = null;
const qortexUrlBase = apiUrl || DEFAULT_API_URL;
const windowUrl = window.top.location.host;
qortexSessionInfo.bidderArray = bidders;
qortexSessionInfo.impressionIds = new Set();
qortexSessionInfo.currentSiteContext = null;
qortexSessionInfo.pageAnalysisdata = {
requestSuccessful: null,
analysisGenerated: false,
contextAdded: {}
};
qortexSessionInfo.sessionId = generateSessionId();
Fixed Show fixed Hide fixed
qortexSessionInfo.groupId = groupId;
qortexSessionInfo.groupConfigUrl = `${qortexUrlBase}/api/v1/prebid/group/configs/${groupId}/${windowUrl}`
qortexSessionInfo.contextUrl = `${qortexUrlBase}/api/v1/prebid/${groupId}/page/lookup`
qortexSessionInfo.pageAnalyisUrl = `${qortexUrlBase}/api/v1/prebid/${groupId}/page/index`;
qortexSessionInfo.analyticsUrl = generateAnalyticsHostUrl(qortexUrlBase);
return qortexSessionInfo;
}

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;
}

export function setContextData(value) {
currentSiteContext = value
qortexSessionInfo.currentSiteContext = value
}

export function setGroupConfigData(value) {
qortexSessionInfo.groupConfig = value
}

/**
* Creates page index data for Qortex analysis
* @returns {Object} page index object
*/
function generateIndexData () {
return {
pageUrl: document.location.href,
title: document.title,
text: document.body.innerText.replaceAll(/\r?\n/gi, ' '),
meta: Array.from(document.getElementsByTagName('meta')).reduce((acc, curr) => { const attr = curr.attributes; if (attr.length > 1) { acc[curr.attributes[0].value] = curr.attributes[1].value } return acc }, {}),
videos: Array.from(document.getElementsByTagName('video')).reduce((acc, curr) => { const src = curr?.src; if (src != '') { acc.push(src) } return acc }, [])
}
}

function generateSessionId() {
const randomInt = Math.floor(Math.random() * 2147483647);
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;
}
data.qortexData = qxData;
data.qortexDataAdded = qxDataAdded;
return data;
}

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

export const qortexSubmodule = {
name: 'qortex',
init,
getBidRequestData
getBidRequestData,
onAuctionEndEvent
}

submodule('realTimeData', qortexSubmodule);
2 changes: 1 addition & 1 deletion modules/qortexRtdProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Maintainer: [email protected]

The Qortex RTD module appends contextual segments to the bidding object based on the content of a page using the Qortex API.

Upon load, the Qortex context API will analyze the bidder page (video, text, image, etc.) and will return a [Content object](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=26). The module will then merge that object into the appropriate bidders' `ortb2.site.content`, which can be used by prebid adapters that use `site.content` data.
If the `Qortex Group Id` provided during configuration is active and enabled for bid enrichment, the Qortex context API will analyze the bidder page (video, text, image, etc.) and will return a [Content object](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=26). The module will then merge that object into the appropriate bidders' `ortb2.site.content`, which can be used by prebid adapters that use `site.content` data.


## Build
Expand Down
Loading