Skip to content

Commit

Permalink
Oxxion Rtd Module: initial module release (prebid#9102)
Browse files Browse the repository at this point in the history
* add oxxionRtdProvider new module

* +

* add tests

* Update oxxionRtdProvider.md

* Update oxxionRtdProvider.js

* Update oxxionRtdProvider_spec.js

* +

* change test

* change tests

* change tests

* change tests

* change tests

* requests changes

* requests changes

* requested changes
  • Loading branch information
matthieularere-msq authored Oct 24, 2022
1 parent b744a9d commit 0a267dd
Show file tree
Hide file tree
Showing 3 changed files with 309 additions and 0 deletions.
119 changes: 119 additions & 0 deletions modules/oxxionRtdProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { submodule } from '../src/hook.js'
import { deepAccess, logInfo } from '../src/utils.js'

const oxxionRtdSearchFor = [ 'adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'userId', 'labelAny', 'adId' ];
const LOG_PREFIX = 'oxxionRtdProvider submodule: ';

const allAdUnits = [];

/** @type {RtdSubmodule} */
export const oxxionSubmodule = {
name: 'oxxionRtd',
init: init,
onAuctionEndEvent: onAuctionEnd,
getBidRequestData: getAdUnits,
};

function init(config, userConsent) {
if (!config.params || !config.params.domain || !config.params.contexts || !Array.isArray(config.params.contexts) || config.params.contexts.length == 0) {
return false
}
return true;
}

function getAdUnits(reqBidsConfigObj, callback, config, userConsent) {
const reqAdUnits = reqBidsConfigObj.adUnits;
if (Array.isArray(reqAdUnits)) {
reqAdUnits.forEach(adunit => {
if (config.params.contexts.includes(deepAccess(adunit, 'mediaTypes.video.context'))) {
allAdUnits.push(adunit);
}
});
}
}

function insertVideoTracking(bidResponse, config, maxCpm) {
if (bidResponse.mediaType === 'video') {
const trackingUrl = getImpUrl(config, bidResponse, maxCpm);
if (!trackingUrl) {
return;
}
// Vast Impression URL
if (bidResponse.vastUrl) {
bidResponse.vastImpUrl = bidResponse.vastImpUrl
? trackingUrl + '&url=' + encodeURI(bidResponse.vastImpUrl)
: trackingUrl
}
// Vast XML document
if (bidResponse.vastXml !== undefined) {
const doc = new DOMParser().parseFromString(bidResponse.vastXml, 'text/xml');
const wrappers = doc.querySelectorAll('VAST Ad Wrapper, VAST Ad InLine');
let hasAltered = false;
if (wrappers.length) {
wrappers.forEach(wrapper => {
const impression = doc.createElement('Impression');
impression.appendChild(doc.createCDATASection(trackingUrl));
wrapper.appendChild(impression)
});
bidResponse.vastXml = new XMLSerializer().serializeToString(doc);
hasAltered = true;
}
if (hasAltered) {
logInfo(LOG_PREFIX + 'insert into vastXml for adId ' + bidResponse.adId);
}
}
}
}

function getImpUrl(config, data, maxCpm) {
const adUnitCode = data.adUnitCode;
const adUnits = allAdUnits.find(adunit => adunit.code === adUnitCode &&
'mediaTypes' in adunit &&
'video' in adunit.mediaTypes &&
typeof adunit.mediaTypes.video.context === 'string');
const context = adUnits !== undefined
? adUnits.mediaTypes.video.context
: 'unknown';
if (!config.params.contexts.includes(context)) {
return false;
}
let trackingImpUrl = 'https://' + config.params.domain + '.oxxion.io/analytics/vast_imp?';
trackingImpUrl += oxxionRtdSearchFor.reduce((acc, param) => {
switch (typeof data[param]) {
case 'string':
case 'number':
acc += param + '=' + data[param] + '&'
break;
}
return acc;
}, '');
const cpmIncrement = Math.round(100000 * (data.cpm - maxCpm)) / 100000;
return trackingImpUrl + 'cpmIncrement=' + cpmIncrement + '&context=' + context;
}

function onAuctionEnd(auctionDetails, config, userConsent) {
const transactionsToCheck = {}
auctionDetails.adUnits.forEach(adunit => {
if (config.params.contexts.includes(deepAccess(adunit, 'mediaTypes.video.context'))) {
transactionsToCheck[adunit.transactionId] = {'bids': {}, 'maxCpm': 0.0, 'secondMaxCpm': 0.0};
}
});
for (const key in auctionDetails.bidsReceived) {
if (auctionDetails.bidsReceived[key].transactionId in transactionsToCheck) {
transactionsToCheck[auctionDetails.bidsReceived[key].transactionId]['bids'][auctionDetails.bidsReceived[key].adId] = {'key': key, 'cpm': auctionDetails.bidsReceived[key].cpm};
if (auctionDetails.bidsReceived[key].cpm > transactionsToCheck[auctionDetails.bidsReceived[key].transactionId]['maxCpm']) {
transactionsToCheck[auctionDetails.bidsReceived[key].transactionId]['secondMaxCpm'] = transactionsToCheck[auctionDetails.bidsReceived[key].transactionId]['maxCpm'];
transactionsToCheck[auctionDetails.bidsReceived[key].transactionId]['maxCpm'] = auctionDetails.bidsReceived[key].cpm;
} else if (auctionDetails.bidsReceived[key].cpm > transactionsToCheck[auctionDetails.bidsReceived[key].transactionId]['secondMaxCpm']) {
transactionsToCheck[auctionDetails.bidsReceived[key].transactionId]['secondMaxCpm'] = auctionDetails.bidsReceived[key].cpm;
}
}
};
Object.keys(transactionsToCheck).forEach(transaction => {
Object.keys(transactionsToCheck[transaction]['bids']).forEach(bid => {
insertVideoTracking(auctionDetails.bidsReceived[transactionsToCheck[transaction]['bids'][bid].key], config, transactionsToCheck[transaction].secondMaxCpm);
});
});
}

submodule('realTimeData', oxxionSubmodule);
48 changes: 48 additions & 0 deletions modules/oxxionRtdProvider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Overview

Module Name: Oxxion Rtd Provider
Module Type: Rtd Provider
Maintainer: tech@oxxion.io

# Oxxion Real-Time-Data submodule

Oxxion helps you to understand how your prebid stack performs.
This Rtd module is to use in order to improve video events tracking.

# Integration

Make sure to have the following modules listed while building prebid : `rtdModule,oxxionRtdProvider`
`rtbModule` is required to activate real-time-data submodules.
For example :
```
gulp build --modules=schain,priceFloors,currency,consentManagement,appnexusBidAdapter,rubiconBidAdapter,rtdModule,oxxionRtdProvider
```

Then add the oxxion Rtd module to your prebid configuration :
```
pbjs.setConfig(
...
realTimeData: {
auctionDelay: 200,
dataProviders: [
{
name: "oxxionRtd",
waitForIt: true,
params: {
domain: "test.endpoint",
contexts: ["instream"],
}
}
]
}
...
)
```

# setConfig Parameters

| Name | Type | Description |
|:---------------------------------|:---------|:------------------------------------------------------------------------------------------------------------|
| domain | String | This string identifies yourself in Oxxion's systems and is provided to you by your Oxxion representative. |
| contexts | Array | Array defining which video contexts to add tracking events into. Values can be instream and/or outstream. |

142 changes: 142 additions & 0 deletions test/spec/modules/oxxionRtdProvider_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {oxxionSubmodule} from 'modules/oxxionRtdProvider.js';
import 'src/prebid.js';

const utils = require('src/utils.js');

const moduleConfig = {
params: {
domain: 'test.endpoint',
contexts: ['instream', 'outstream']
}
};

let request = {
'auctionId': '1e8b993d-8f0a-4232-83eb-3639ddf3a44b',
'timestamp': 1647424261187,
'auctionEnd': 1647424261714,
'auctionStatus': 'completed',
'adUnits': [
{
'code': 'msq_tag_200124_banner',
'mediaTypes': { 'banner': { 'sizes': [[300, 600]] } },
'bids': [{'bidder': 'appnexus', 'params': {'placementId': 123456}}],
'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b40'
},
{
'code': 'msq_tag_200125_video',
'mediaTypes': { 'video': { 'context': 'instream' }, playerSize: [640, 480], mimes: ['video/mp4'] },
'bids': [
{'bidder': 'mediasquare', 'params': {'code': 'publishername_atf_desktop_rg_video', 'owner': 'test'}},
{'bidder': 'appnexusAst', 'params': {'placementId': 345678}},
],
'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b41'
},
]
};

let bids = [{
'bidderCode': 'mediasquare',
'width': 640,
'height': 480,
'statusMessage': 'Bid available',
'adId': '3647626fdbe68a',
'requestId': '2d891705d2125b',
'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b41',
'auctionId': '1e8b993d-8f0a-4232-83eb-3639ddf3a44b',
'mediaType': 'video',
'source': 'client',
'cpm': 0.9723,
'creativeId': 'freewheel|AdswizzAd71819',
'currency': 'USD',
'netRevenue': true,
'ttl': 2000,
'mediasquare': {
'bidder': 'freewheel',
'code': 'test/publishername_atf_desktop_rg_video',
'hasConsent': true
},
'meta': {
'advertiserDomains': [
'unknown'
]
},
'vastUrl': 'https://some.vast-url.com',
'vastXml': '<VAST version="3.0"><Ad><Wrapper><VASTAdTagURI><![CDATA[https://pbs-front.mediasquare.fr/cache?uuid=4de68767e8f2f9974fd4addd5a9d135a]]></VASTAdTagURI><Impression><![CDATA[https://pbs-front.mediasquare.fr/winning?adUnitCode=&auctionId=ZltsQnBqbcyC6aalwAdD4irnXmrl3E&bidder=freewheel&code=test/publishername_atf_desktop_rg_video&cpm=0.9723&creativeId=AdswizzAd71819&currency=USD&hasConsent=true&mediaType=video&pbjs=&requestId=2d891705d2125b&size=video&timeToRespond=unknown]]></Impression></Wrapper></Ad></VAST>',
'adapterCode': 'mediasquare',
'originalCpm': 0.9723,
'originalCurrency': 'USD',
'responseTimestamp': 1665505150740,
'requestTimestamp': 1665505150594,
'bidder': 'mediasquare',
'adUnitCode': 'msq_tag_200125_video',
'timeToRespond': 146,
'size': '640x480',
}, {
'bidderCode': 'appnexusAst',
'width': 640,
'height': 480,
'statusMessage': 'Bid available',
'adId': '4b2e1581c0ca1a',
'requestId': '2d891705d2125b',
'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b41',
'auctionId': '1e8b993d-8f0a-4232-83eb-3639ddf3a44b',
'mediaType': 'video',
'source': 'client',
'cpm': 1.9723,
'creativeId': '159080650',
'currency': 'USD',
'netRevenue': true,
'ttl': 2000,
'vastUrl': 'https://some.vast-url.com',
'vastXml': '<VAST version="3.0"><Ad><InLine><AdSystem>Adnxs</AdSystem><AdTitle>Title</AdTitle><Creatives/></InLine></Ad></VAST>',
'adapterCode': 'mediasquare',
'originalCpm': 1.9723,
'originalCurrency': 'USD',
'responseTimestamp': 1665505150740,
'requestTimestamp': 1665505150594,
'bidder': 'appnexusAst',
'adUnitCode': 'msq_tag_200125_video',
'timeToRespond': 146,
'size': '640x480',
'vastImpUrl': 'https://some.tracking-url.com'
},
];

describe('oxxionRtdProvider', () => {
describe('Oxxion RTD sub module', () => {
it('should init, return true, and set the params', () => {
expect(oxxionSubmodule.init(moduleConfig)).to.equal(true);
});
});

describe('Oxxion RTD sub module', () => {
let auctionEnd = request;
auctionEnd.bidsReceived = bids;
it('call everything', function() {
oxxionSubmodule.getBidRequestData(request, null, moduleConfig);
oxxionSubmodule.onAuctionEndEvent(auctionEnd, moduleConfig);
});
it('check vastImpUrl', function() {
expect(auctionEnd.bidsReceived[0]).to.have.property('vastImpUrl');
let expectVastImpUrl = 'https://' + moduleConfig.params.domain + '.oxxion.io/analytics/vast_imp?';
expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(expectVastImpUrl);
expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(encodeURI('https://some.tracking-url.com'));
});
it('check vastXml', function() {
expect(auctionEnd.bidsReceived[0]).to.have.property('vastXml');
let vastWrapper = new DOMParser().parseFromString(auctionEnd.bidsReceived[0].vastXml, 'text/xml');
let impressions = vastWrapper.querySelectorAll('VAST Ad Wrapper Impression');
expect(impressions.length).to.equal(2);
expect(auctionEnd.bidsReceived[1]).to.have.property('vastXml');
expect(auctionEnd.bidsReceived[1].adId).to.equal('4b2e1581c0ca1a');
let vastInline = new DOMParser().parseFromString(auctionEnd.bidsReceived[1].vastXml, 'text/xml');
let inline = vastInline.querySelectorAll('VAST Ad InLine');
expect(inline).to.have.lengthOf(1);
let inlineImpressions = vastInline.querySelectorAll('VAST Ad InLine Impression');
expect(inlineImpressions).to.have.lengthOf.above(0);
});
it('check cpmIncrement', function() {
expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(encodeURI('cpmIncrement=1'));
});
});
});

0 comments on commit 0a267dd

Please sign in to comment.