Skip to content

Commit

Permalink
added instream BID_WON tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
monisq committed Aug 5, 2020
1 parent c3a193c commit 1b67ebc
Show file tree
Hide file tree
Showing 5 changed files with 320 additions and 5 deletions.
9 changes: 9 additions & 0 deletions karma.conf.maker.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ module.exports = function(codeCoverage, browserstack, watchMode, file) {

plugins: plugins
}

// add preprocessors when file is passed
if (file) {
config.files.forEach((file) => {
config.preprocessors[file] = ['webpack', 'sourcemap'];
});
delete config.preprocessors['test/test_index.js'];
}

setReporters(config, codeCoverage, browserstack);
setBrowsers(config, browserstack);
return config;
Expand Down
13 changes: 9 additions & 4 deletions modules/dfpAdServerVideo.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, bui
import { config } from '../src/config.js';
import { getHook, submodule } from '../src/hook.js';
import { auctionManager } from '../src/auctionManager.js';
import events from '../src/events.js';
import CONSTANTS from '../src/constants.json';

/**
* @typedef {Object} DfpVideoParams
Expand Down Expand Up @@ -245,17 +247,20 @@ function getCustParams(bid, options) {
allTargetingData = (allTargeting) ? allTargeting[adUnit.code] : {};
}

const optCustParams = deepAccess(options, 'params.cust_params');
let customParams = Object.assign({},
const prebidTargetingSet = Object.assign({},
// Why are we adding standard keys here ? Refer https://github.com/prebid/Prebid.js/issues/3664
{ hb_uuid: bid && bid.videoCacheKey },
// hb_uuid will be deprecated and replaced by hb_cache_id
{ hb_cache_id: bid && bid.videoCacheKey },
allTargetingData,
adserverTargeting,
optCustParams,
);
return encodeURIComponent(formatQS(customParams));
events.emit(CONSTANTS.EVENTS.SET_TARGETING, {[adUnit.code]: prebidTargetingSet});

// merge the prebid + publisher targeting sets
const publisherTargetingSet = deepAccess(options, 'params.cust_params');
const targetingSet = Object.assign({}, prebidTargetingSet, publisherTargetingSet);
return encodeURIComponent(formatQS(targetingSet));
}

registerVideoSupport('dfp', {
Expand Down
4 changes: 3 additions & 1 deletion src/auction.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { hook } from './hook.js';
import find from 'core-js-pure/features/array/find.js';
import { OUTSTREAM } from './video.js';
import { VIDEO } from './mediaTypes.js';
import { trackInstreamDeliveredImpressions } from './instreamTracking.js';

const { syncUsers } = userSync;
const utils = require('./utils.js');
Expand All @@ -93,7 +94,7 @@ const queuedCalls = [];
* Creates new auction instance
*
* @param {Object} requestConfig
* @param {AdUnit} requestConfig.adUnits
* @param {Array<AdUnit>} requestConfig.adUnits
* @param {AdUnitCode} requestConfig.adUnitCodes
* @param {function():void} requestConfig.callback
* @param {number} requestConfig.cbTimeout
Expand Down Expand Up @@ -167,6 +168,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a
_auctionEnd = Date.now();

events.emit(CONSTANTS.EVENTS.AUCTION_END, getProperties());
trackInstreamDeliveredImpressions(_adUnits, _bidsReceived);
bidsBackCallback(_adUnits, function () {
try {
if (_callback != null) {
Expand Down
106 changes: 106 additions & 0 deletions src/instreamTracking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { config } from './config.js';
import { auctionManager } from './auctionManager.js';
import { VIDEO } from './mediaTypes.js';
import { OUTSTREAM } from './video.js';
import * as events from './events.js';
import * as utils from './utils.js';
import { TARGETING_KEYS, EVENTS, BID_STATUS } from '../src/constants.json';

const { CACHE_ID, UUID } = TARGETING_KEYS;
const { BID_WON } = EVENTS;
const { RENDERED } = BID_STATUS;

const INSTREAM_TRACKING_DEFAULT_CONFIG = {
enabled: false,
maxWindow: 1000 * 60, // the time in ms after which polling for instream delivery stops
pollingFreq: 500 // the frequency of polling
};

// Set instreamTracking default values
config.setDefaults({
'instreamTracking': utils.deepClone(INSTREAM_TRACKING_DEFAULT_CONFIG)
});

const whitelistedResources = /video|fetch|xmlhttprequest|other/;

/**
* Here the idea is
* find all network entries via performance.getEntriesByType()
* filter it by video cache key in the url
* and exclude the ad server urls so that we dont match twice
* eg:
* dfp ads call: https://securepubads.g.doubleclick.net/gampad/ads?...hb_cache_id%3D55e85cd3-6ea4-4469-b890-84241816b131%26...
* prebid cache url: https://prebid.adnxs.com/pbc/v1/cache?uuid=55e85cd3-6ea4-4469-b890-84241816b131
*
* if the entry exists, emit the BID_WON
*
* Note: this is a workaround till a better approach is engineered.
*
* @param {Array<AdUnit>} adUnits
* @param {Array<Bid>} bidsReceived
*
* @return {boolean} returns TRUE if tracking started
*/
export function trackInstreamDeliveredImpressions(adUnits, bidsReceived) {
const instreamTrackingConfig = config.getConfig('instreamTracking') || {};
// check if instreamTracking is enabled and performance api is available
if (!instreamTrackingConfig.enabled || !window.performance || !window.performance.getEntriesByType) {
return false;
}

// filter for video bids
const instreamBids = bidsReceived.filter(bid => bid.mediaType === VIDEO && bid.context !== OUTSTREAM && bid.videoCacheKey);
if (!instreamBids.length) {
return false;
}

// find unique instream ad units
const instreamAdUnitMap = {}
adUnits.forEach(adUnit => {
if (!instreamAdUnitMap[adUnit.code] && utils.deepAccess(adUnit, 'mediaTypes.video.context') !== OUTSTREAM) {
instreamAdUnitMap[adUnit.code] = true;
}
});
const instreamAdUnitsCount = Object.keys(instreamAdUnitMap).length;

const start = Date.now();
const { maxWindow, pollingFreq } = instreamTrackingConfig;

let instreamWinningBidsCount = 0;
let lastRead = 0; // offset for performance.getEntriesByType

function poll() {
// get network entries using the last read offset
const entries = window.performance.getEntriesByType('resource').splice(lastRead);
for (const resource of entries) {
const url = resource.name;
// check if the resource is of whitelisted resource to avoid checking img or css or script urls
if (!whitelistedResources.test(resource.initiatorType)) {
continue;
}

instreamBids.forEach((bid) => {
// match the video cache key excluding ad server call
const matches = !(url.indexOf(CACHE_ID) !== -1 || url.indexOf(UUID) !== -1) && url.indexOf(bid.videoCacheKey) !== -1;
if (matches && bid.status !== RENDERED) {
// video found
instreamWinningBidsCount++;
auctionManager.addWinningBid(bid);
events.emit(BID_WON, bid);
}
});
}
// update offset
lastRead += entries.length;

const timeElapsed = Date.now() - start;
if (timeElapsed < maxWindow && instreamWinningBidsCount < instreamAdUnitsCount) {
setTimeout(poll, pollingFreq);
}
}

// start polling for network entries
setTimeout(poll, pollingFreq);

return true;
}
193 changes: 193 additions & 0 deletions test/spec/instreamTracking_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { assert } from 'chai';
import { trackInstreamDeliveredImpressions } from 'src/instreamTracking.js';
import { config } from 'src/config.js';
import * as events from 'src/events.js';
import * as sinon from 'sinon';
import { INSTREAM, OUTSTREAM } from 'src/video.js';

const BIDDER_CODE = 'sampleBidder';
const VIDEO_CACHE_KEY = '4cf395af-8fee-4960-af0e-88d44e399f14';

let sandbox;

function enableInstreamTracking() {
let configStub = sandbox.stub(config, 'getConfig');
configStub.withArgs('instreamTracking').returns({
enabled: true,
maxWindow: 10,
pollingFreq: 0
});
}

function mockPerformanceApi({adServerCallSent, videoPresent}) {
let performanceStub = sandbox.stub(window.performance, 'getEntriesByType');
let entries = [{
name: 'https://domain.com/img.png',
initiatorType: 'img'
}, {
name: 'https://domain.com/script.js',
initiatorType: 'script'
}, {
name: 'https://domain.com/xhr',
initiatorType: 'xmlhttprequest'
}, {
name: 'https://domain.com/fetch',
initiatorType: 'fetch'
}];

if (adServerCallSent || videoPresent) {
entries.push({
name: 'https://adserver.com/ads?custom_params=hb_uuid%3D' + VIDEO_CACHE_KEY + '%26pos%3D' + VIDEO_CACHE_KEY,
initiatorType: 'xmlhttprequest'
});
}

if (videoPresent) {
entries.push({
name: 'https://prebid-vast-cache.com/cache?key=' + VIDEO_CACHE_KEY,
initiatorType: 'xmlhttprequest'
});
}

performanceStub.withArgs('resource').returns(entries);
}

function getVideoBid(context) {
return {
'bidderCode': BIDDER_CODE,
'width': 640,
'height': 480,
'statusMessage': 'Bid available',
'adId': '20372a557635064',
'requestId': '16ee6b19daeb431',
'mediaType': 'video',
'context': context,
'video': {
'context': context,
},
'source': 'client',
'no_bid': false,
'cpm': 1.1495,
'ad': "<div id=''></div>",
'ttl': 180,
'creativeId': '7C19U58N2DL-120340442-30-11',
'netRevenue': true,
'currency': 'USD',
'videoCacheKey': VIDEO_CACHE_KEY,
}
}

describe('Instream Tracking', function () {
beforeEach(function () {
sandbox = sinon.sandbox.create();
});

afterEach(function () {
sandbox.restore();
});

describe('gaurd checks', function () {
it('skip if tracking not enable', function () {
sandbox.stub(config, 'getConfig').withArgs('instreamTracking').returns(undefined);
assert.isNotOk(trackInstreamDeliveredImpressions([], []), 'should not start tracking when tracking is disabled');
});

it('run only if instream ad units are present', function () {
enableInstreamTracking();
assert.isNotOk(trackInstreamDeliveredImpressions([], []));
});

it('checks for instream bids', function (done) {
enableInstreamTracking();
const adUnits = [{
code: 'banner',
mediaType: {banner: {sizes: [[300, 250]]}},
bids: [{bidder: BIDDER_CODE, params: {placementId: 'id'}}]
}];

const bidsReceived = [{
'bidderCode': BIDDER_CODE,
'width': 300,
'height': 250,
'statusMessage': 'Bid available',
'adId': '20372a557635064',
'requestId': '16ee6b19daeb431',
'mediaType': 'banner',
'source': 'client',
'no_bid': false,
'cpm': 1.1495,
'ad': "<div id=''></div>",
'ttl': 180,
'creativeId': '7C19U58N2DL-120340442-30-11',
'netRevenue': true,
'currency': 'USD',
}];
assert.isNotOk(trackInstreamDeliveredImpressions(adUnits, bidsReceived), 'should not start tracking when banner bids are present')
adUnits.push({
code: 'video-' + OUTSTREAM,
mediaType: {video: {playerSize: [640, 480], context: OUTSTREAM}},
bids: [{bidder: BIDDER_CODE, params: {placementId: 'id'}}]
})
bidsReceived.push(getVideoBid(OUTSTREAM));
assert.isNotOk(trackInstreamDeliveredImpressions(adUnits, bidsReceived), 'should not start tracking when outstream bids are present')

adUnits.push({
code: 'video-' + INSTREAM,
mediaType: {video: {playerSize: [640, 480], context: INSTREAM}},
bids: [{bidder: BIDDER_CODE, params: {placementId: 'id'}}]
})
bidsReceived.push(getVideoBid(INSTREAM));
mockPerformanceApi({});
assert.isOk(trackInstreamDeliveredImpressions(adUnits, bidsReceived), 'should start tracking when instream bids are present')
setTimeout(done, 10);
});
});

describe('instream bids check', function () {
let adUnits;
let bidsReceived;
let spyEventsOn;

beforeEach(function () {
enableInstreamTracking();
adUnits = [{
code: 'video-' + INSTREAM,
mediaType: {video: {playerSize: [640, 480], context: INSTREAM}},
bids: [{bidder: BIDDER_CODE, params: {placementId: 'id'}}]
}];
bidsReceived = [getVideoBid(INSTREAM)];
spyEventsOn = sandbox.spy(events, 'emit');
});

it('BID WON event is not emitted when no video cache key entries are present', function (done) {
trackInstreamDeliveredImpressions(adUnits, bidsReceived);
mockPerformanceApi({});
setTimeout(function () {
assert.isNotOk(spyEventsOn.calledWith('bidWon'))
done()
}, 10);
});

it('BID WON event is not emitted when ad server call is sent', function (done) {
mockPerformanceApi({adServerCallSent: true});
setTimeout(function () {
assert.isNotOk(spyEventsOn.calledWith('bidWon'))
done()
}, 10);
});

it('BID WON event is emitted when video cache key is present', function (done) {
const bidWonSpy = sandbox.spy();
events.on('bidWon', bidWonSpy);
mockPerformanceApi({adServerCallSent: true, videoPresent: true});

trackInstreamDeliveredImpressions(adUnits, bidsReceived);

setTimeout(function () {
assert.isOk(spyEventsOn.calledWith('bidWon'))
assert(bidWonSpy.args[0][0].videoCacheKey, VIDEO_CACHE_KEY, 'Video cache key in bid won should be equal to video cache call');
done()
}, 10);
});
});
});

0 comments on commit 1b67ebc

Please sign in to comment.