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

Qortex RTD Provider: initial release #10480

Merged
merged 3 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
165 changes: 165 additions & 0 deletions modules/qortexRtdProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { submodule } from '../src/hook.js';
import { ajax } from '../src/ajax.js';
import { logWarn, mergeDeep, logMessage, generateUUID } from '../src/utils.js';
import { loadExternalScript } from '../src/adloader.js';
import * as events from '../src/events.js';
import CONSTANTS from '../src/constants.json';

let requestUrl;
let bidderArray;
let impressionIds;
let currentSiteContext;

/**
* Init if module configuration is valid
* @param {Object} config Module configuration
* @returns {Boolean}
*/
function init (config) {
if (!config?.params?.groupId?.length > 0) {
logWarn('Qortex RTD module config does not contain valid groupId parameter. Config params: ' + JSON.stringify(config.params))
return false;
} else {
initializeModuleData(config);
}
if (config?.params?.tagConfig) {
loadScriptTag(config)
}
return true;
}

/**
* Processess prebid request and attempts to add context to ort2b fragments
* @param {Object} reqBidsConfig Bid request configuration object
* @param {Function} callback Called on completion
*/
function getBidRequestData (reqBidsConfig, callback) {
if (reqBidsConfig?.adUnits?.length > 0) {
getContext()
.then(contextData => {
setContextData(contextData)
addContextToRequests(reqBidsConfig)
callback();
})
.catch((e) => {
logWarn(e?.message);
callback();
});
} else {
logWarn('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfig))
callback();
}
}

/**
* determines whether to send a request to context api and does so if necessary
* @returns {Promise} ortb Content object
*/
export function getContext () {
if (!currentSiteContext) {
logMessage('Requesting new context data');
return new Promise((resolve, reject) => {
const callbacks = {
success(text, data) {
const result = data.status === 200 ? JSON.parse(data.response)?.content : null;
resolve(result);
},
error(error) {
reject(new Error(error));
}
}
ajax(requestUrl, callbacks)
})
} else {
logMessage('Adding Content object from existing context data');
return new Promise(resolve => resolve(currentSiteContext));
}
}

/**
* Updates bidder configs with the response from Qortex context services
* @param {Object} reqBidsConfig Bid request configuration object
* @param {string[]} bidders Bidders specified in module's configuration
*/
export function addContextToRequests (reqBidsConfig) {
if (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) {
mergeDeep(reqBidsConfig.ortb2Fragments.global, fragment);
} else {
logWarn('Config contains an empty bidders array, unable to determine which bids to enrich');
}
}
}

/**
* Loads Qortex header tag using data passed from module config object
* @param {Object} config module config obtained during init
*/
export function loadScriptTag(config) {
const code = 'qortex';
const groupId = config.params.groupId;
const src = 'https://tags.qortex.ai/bootstrapper'
const attr = {'data-group-id': groupId}
const tc = config.params.tagConfig

Object.keys(tc).forEach(p => {
attr[`data-${p.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`)}`] = tc[p]
})

addEventListener('qortex-rtd', (e) => {
const billableEvent = {
vendor: code,
billingId: generateUUID(),
type: e?.detail?.type,
accountId: groupId
}
switch (e?.detail?.type) {
case 'qx-impression':
const {uid} = e.detail;
if (!uid || impressionIds.has(uid)) {
logWarn(`recieved invalid billable event due to ${!uid ? 'missing' : 'duplicate'} uid: qx-impression`)
return;
} else {
logMessage('recieved billable event: qx-impression')
impressionIds.add(uid)
billableEvent.transactionId = e.detail.uid;
events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, billableEvent);
break;
}
default:
logWarn(`recieved invalid billable event: ${e.detail?.type}`)
}
})

loadExternalScript(src, code, undefined, undefined, attr);
}

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

export function setContextData(value) {
currentSiteContext = value
}

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

submodule('realTimeData', qortexSubmodule);
69 changes: 69 additions & 0 deletions modules/qortexRtdProvider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Qortex Real-time Data Submodule

## Overview

```
Module Name: Qortex RTD Provider
Module Type: RTD Provider
Maintainer: mannese@qortex.ai
```

## Description

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.


## Build
```
gulp build --modules="rtdModule,qortexRtdProvider,qortexBidAdapter,..."
```

> `rtdModule` is a required module to use Qortex RTD module.

## Configuration

Please refer to [Prebid Documentation](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-realTimeData) on RTD module configuration for details on required and optional parameters of `realTimeData`

When configuring Qortex as a data provider, refer to the template below to add the necessary information to ensure the proper connection is made.

### RTD Module Setup

```javascript
pbjs.setConfig({
realTimeData: {
auctionDelay: 1000,
dataProviders: [{
name: 'qortex',
waitForIt: true,
params: {
groupId: 'ABC123', //required
bidders: ['qortex', 'adapter2'], //optional (see below)
tagConfig: { // optional, please reach out to your account manager for configuration reccommendation
videoContainer: 'string',
htmlContainer: 'string',
attachToTop: 'string',
esm6Mod: 'string',
continuousLoad: 'string'
}
}
}]
}
});
```

### Paramter Details

#### `groupId` - Required
- The Qortex groupId linked to the publisher, this is required to make a request using this adapter

#### `bidders` - optional
- If this parameter is included, it must be an array of the strings that match the bidder code of the prebid adapters you would like this module to impact. `ortb2.site.content` will be updated *only* for adapters in this array

- If this parameter is omitted, the RTD module will default to updating `ortb2.site.content` on *all* bid adapters being used on the page

#### `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.
1 change: 1 addition & 0 deletions src/adloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const _approvedLoadExternalJSList = [
'clean.io',
'a1Media',
'geoedge',
'qortex'
]

/**
Expand Down
Loading