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