diff --git a/libraries/vastTrackers/vastTrackers.js b/libraries/vastTrackers/vastTrackers.js
new file mode 100644
index 00000000000..b4ae98aba57
--- /dev/null
+++ b/libraries/vastTrackers/vastTrackers.js
@@ -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;
+}
diff --git a/src/videoCache.js b/src/videoCache.js
index 88fc27625fd..ce03f2f624e 100644
--- a/src/videoCache.js
+++ b/src/videoCache.js
@@ -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) ? `` : ``;
+ let impressions = impTrackerURLs ? impTrackerURLs.map(trk => ``).join('') : '';
return `
prebid.org wrapper
- ${vastImp}
+ ${impressions}
diff --git a/test/spec/libraries/vastTrackers_spec.js b/test/spec/libraries/vastTrackers_spec.js
new file mode 100644
index 00000000000..3849ea75b02
--- /dev/null
+++ b/test/spec/libraries/vastTrackers_spec.js
@@ -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 = '';
+ vastXml = insertVastTrackers(trackers, vastXml);
+ expect(vastXml).to.equal('');
+ });
+
+ 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;
+ });
+})
diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js
index c746fdd2afd..fc6e71779cb 100644
--- a/test/spec/videoCache_spec.js
+++ b/test/spec/videoCache_spec.js
@@ -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();
@@ -127,7 +127,7 @@ describe('The video cache', function () {
prebid.org wrapper
-
+
@@ -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 = `
+
+
+ prebid.org wrapper
+
+
+
+
+
+ `;
+ 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 = '';
assertRequestMade({ vastXml: vastXml, ttl: 25 }, vastXml);