Skip to content

Commit

Permalink
PBjs Core : add ability to inject tracking in video (#10191)
Browse files Browse the repository at this point in the history
* add vast impression tracking

* support additional context macro

* fix spaces and singlequotes

* remove 2494945CONTEXT2494945 macro

* remove CONTEXT macro

* do not update vastImpUrl anymore

* add impression trackers in video cache

* insert ony unique trackers

* rename registerVastTrackers

* rename arrayVastTrackers

* trackers object change

* check modules are allowed to add trackers based on isActivityAllowed

* rename validVastTracker and add line breaks

* removes duplicates verification in isValidVastTracker

* changes in wrapURI + typo fix

* requested changes

* update function trackersToMap

* using Set in trackers map

* changes suggested by dgirardi

* changes suggested by dgirardi

* Update test/spec/video_spec.js

Co-authored-by: Karim Mourra <[email protected]>

* add spaces

---------

Co-authored-by: Karim Mourra <[email protected]>
  • Loading branch information
matthieularere-msq and karimMourra authored Feb 7, 2024
1 parent d8c2ffd commit 9f5e205
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 8 deletions.
95 changes: 95 additions & 0 deletions libraries/vastTrackers/vastTrackers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {addBidResponse} from '../../src/auction.js';
import {VIDEO} from '../../src/mediaTypes.js';
import {logError} from '../../src/utils.js';
import {isActivityAllowed} from '../../src/activities/rules.js';
import {ACTIVITY_REPORT_ANALYTICS} from '../../src/activities/activities.js';
import {activityParams} from '../../src/activities/activityParams.js';

const vastTrackers = [];

addBidResponse.before(function (next, adUnitcode, bidResponse, reject) {
if (FEATURES.VIDEO && bidResponse.mediaType === VIDEO) {
const vastTrackers = getVastTrackers(bidResponse);
if (vastTrackers) {
bidResponse.vastXml = insertVastTrackers(vastTrackers, bidResponse.vastXml);
const impTrackers = vastTrackers.get('impressions');
if (impTrackers) {
bidResponse.vastImpUrl = [].concat(impTrackers).concat(bidResponse.vastImpUrl).filter(t => t);
}
}
}
next(adUnitcode, bidResponse, reject);
});

export function registerVastTrackers(moduleType, moduleName, trackerFn) {
if (typeof trackerFn === 'function') {
vastTrackers.push({'moduleType': moduleType, 'moduleName': moduleName, 'trackerFn': trackerFn});
}
}

export function insertVastTrackers(trackers, vastXml) {
const doc = new DOMParser().parseFromString(vastXml, 'text/xml');
const wrappers = doc.querySelectorAll('VAST Ad Wrapper, VAST Ad InLine');
try {
if (wrappers.length) {
wrappers.forEach(wrapper => {
if (trackers.get('impressions')) {
trackers.get('impressions').forEach(trackingUrl => {
const impression = doc.createElement('Impression');
impression.appendChild(doc.createCDATASection(trackingUrl));
wrapper.appendChild(impression);
});
}
});
vastXml = new XMLSerializer().serializeToString(doc);
}
} catch (error) {
logError('an error happened trying to insert trackers in vastXml');
}
return vastXml;
}

export function getVastTrackers(bid) {
let trackers = [];
vastTrackers.filter(
({
moduleType,
moduleName,
trackerFn
}) => isActivityAllowed(ACTIVITY_REPORT_ANALYTICS, activityParams(moduleType, moduleName))
).forEach(({trackerFn}) => {
let trackersToAdd = trackerFn(bid);
trackersToAdd.forEach(trackerToAdd => {
if (isValidVastTracker(trackers, trackerToAdd)) {
trackers.push(trackerToAdd);
}
});
});
const trackersMap = trackersToMap(trackers);
return (trackersMap.size ? trackersMap : null);
};

function isValidVastTracker(trackers, trackerToAdd) {
return trackerToAdd.hasOwnProperty('event') && trackerToAdd.hasOwnProperty('url');
}

function trackersToMap(trackers) {
return trackers.reduce((map, {url, event}) => {
!map.has(event) && map.set(event, new Set());
map.get(event).add(url);
return map;
}, new Map());
}

export function addImpUrlToTrackers(bid, trackersMap) {
if (bid.vastImpUrl) {
if (!trackersMap) {
trackersMap = new Map();
}
if (!trackersMap.get('impressions')) {
trackersMap.set('impressions', new Set());
}
trackersMap.get('impressions').add(bid.vastImpUrl);
}
return trackersMap;
}
7 changes: 4 additions & 3 deletions src/videoCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,18 @@ const ttlBufferInSeconds = 15;
* @param {string} impUrl An impression tracker URL for the delivery of the video ad
* @return A VAST URL which loads XML from the given URI.
*/
function wrapURI(uri, impUrl) {
function wrapURI(uri, impTrackerURLs) {
impTrackerURLs = impTrackerURLs && (Array.isArray(impTrackerURLs) ? impTrackerURLs : [impTrackerURLs]);
// Technically, this is vulnerable to cross-script injection by sketchy vastUrl bids.
// We could make sure it's a valid URI... but since we're loading VAST XML from the
// URL they provide anyway, that's probably not a big deal.
let vastImp = (impUrl) ? `<![CDATA[${impUrl}]]>` : ``;
let impressions = impTrackerURLs ? impTrackerURLs.map(trk => `<Impression><![CDATA[${trk}]]></Impression>`).join('') : '';
return `<VAST version="3.0">
<Ad>
<Wrapper>
<AdSystem>prebid.org wrapper</AdSystem>
<VASTAdTagURI><![CDATA[${uri}]]></VASTAdTagURI>
<Impression>${vastImp}</Impression>
${impressions}
<Creatives></Creatives>
</Wrapper>
</Ad>
Expand Down
33 changes: 33 additions & 0 deletions test/spec/libraries/vastTrackers_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {addImpUrlToTrackers, getVastTrackers, insertVastTrackers, registerVastTrackers} from 'libraries/vastTrackers/vastTrackers.js';
import {MODULE_TYPE_ANALYTICS} from '../../../src/activities/modules.js';

describe('vast trackers', () => {
it('insert into tracker list', function() {
let trackers = getVastTrackers({'cpm': 1.0});
if (!trackers || !trackers.get('impressions')) {
registerVastTrackers(MODULE_TYPE_ANALYTICS, 'test', function(bidResponse) {
return [
{'event': 'impressions', 'url': `https://vasttracking.mydomain.com/vast?cpm=${bidResponse.cpm}`}
];
});
}
trackers = getVastTrackers({'cpm': 1.0});
expect(trackers).to.be.a('map');
expect(trackers.get('impressions')).to.exists;
expect(trackers.get('impressions').has('https://vasttracking.mydomain.com/vast?cpm=1')).to.be.true;
});

it('insert trackers in vastXml', function() {
const trackers = getVastTrackers({'cpm': 1.0});
let vastXml = '<VAST><Ad><Wrapper></Wrapper></Ad></VAST>';
vastXml = insertVastTrackers(trackers, vastXml);
expect(vastXml).to.equal('<VAST><Ad><Wrapper><Impression><![CDATA[https://vasttracking.mydomain.com/vast?cpm=1]]></Impression></Wrapper></Ad></VAST>');
});

it('test addImpUrlToTrackers', function() {
const trackers = addImpUrlToTrackers({'vastImpUrl': 'imptracker.com'}, getVastTrackers({'cpm': 1.0}));
expect(trackers).to.be.a('map');
expect(trackers.get('impressions')).to.exists;
expect(trackers.get('impressions').has('imptracker.com')).to.be.true;
});
})
24 changes: 19 additions & 5 deletions test/spec/videoCache_spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import chai from 'chai';
import { getCacheUrl, store } from 'src/videoCache.js';
import { config } from 'src/config.js';
import { server } from 'test/mocks/xhr.js';
import {getCacheUrl, store} from 'src/videoCache.js';
import {config} from 'src/config.js';
import {server} from 'test/mocks/xhr.js';
import {auctionManager} from '../../src/auctionManager.js';
import {AuctionIndex} from '../../src/auctionIndex.js';
import { batchingCache } from '../../src/auction.js';
import {batchingCache} from '../../src/auction.js';

const should = chai.should();

Expand Down Expand Up @@ -127,7 +127,7 @@ describe('The video cache', function () {
<Wrapper>
<AdSystem>prebid.org wrapper</AdSystem>
<VASTAdTagURI><![CDATA[my-mock-url.com]]></VASTAdTagURI>
<Impression></Impression>
<Creatives></Creatives>
</Wrapper>
</Ad>
Expand All @@ -149,6 +149,20 @@ describe('The video cache', function () {
assertRequestMade({ vastUrl: 'my-mock-url.com', vastImpUrl: 'imptracker.com', ttl: 25 }, expectedValue)
});

it('should include multiple vastImpUrl when it\'s an array', function() {
const expectedValue = `<VAST version="3.0">
<Ad>
<Wrapper>
<AdSystem>prebid.org wrapper</AdSystem>
<VASTAdTagURI><![CDATA[my-mock-url.com]]></VASTAdTagURI>
<Impression><![CDATA[https://vasttracking.mydomain.com/vast?cpm=1.2]]></Impression><Impression><![CDATA[imptracker.com]]></Impression>
<Creatives></Creatives>
</Wrapper>
</Ad>
</VAST>`;
assertRequestMade({ vastUrl: 'my-mock-url.com', vastImpUrl: ['https://vasttracking.mydomain.com/vast?cpm=1.2', 'imptracker.com'], ttl: 25, cpm: 1.2 }, expectedValue)
});

it('should make the expected request when store() is called on an ad with vastXml', function () {
const vastXml = '<VAST version="3.0"></VAST>';
assertRequestMade({ vastXml: vastXml, ttl: 25 }, vastXml);
Expand Down

0 comments on commit 9f5e205

Please sign in to comment.