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 21, 2020
1 parent 8513f13 commit f2df223
Show file tree
Hide file tree
Showing 3 changed files with 344 additions and 4 deletions.
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
114 changes: 114 additions & 0 deletions modules/instreamTracking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { config } from '../src/config.js';
import { auctionManager } from '../src/auctionManager.js';
import { INSTREAM } from '../src/video.js';
import * as events from '../src/events.js';
import * as utils from '../src/utils.js';
import { BID_STATUS, EVENTS, TARGETING_KEYS } from '../src/constants.json';

const {CACHE_ID, UUID} = TARGETING_KEYS;
const {BID_WON, AUCTION_END} = 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
* @param {Array<BidRequest>} bidderRequests
*
* @return {boolean} returns TRUE if tracking started
*/
export function trackInstreamDeliveredImpressions({adUnits, bidsReceived, bidderRequests}) {
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 => {
const bidderRequest = utils.getBidRequest(bid.requestId, bidderRequests);
return bidderRequest && utils.deepAccess(bidderRequest, 'mediaTypes.video.context') === INSTREAM && 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') === INSTREAM) {
instreamAdUnitMap[adUnit.code] = true;
}
});
const instreamAdUnitsCount = Object.keys(instreamAdUnitMap).length;

const start = Date.now();
const {maxWindow, pollingFreq, urlPattern} = 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 (urlPattern && urlPattern instanceof RegExp && !urlPattern.test(url)) {
return;
}
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;
}

events.on(AUCTION_END, trackInstreamDeliveredImpressions)
221 changes: 221 additions & 0 deletions test/spec/modules/instreamTracking_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { assert } from 'chai';
import { trackInstreamDeliveredImpressions } from 'modules/instreamTracking.js';
import { config } from 'src/config.js';
import * as events from 'src/events.js';
import * as utils from 'src/utils.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(regex) {
let configStub = sandbox.stub(config, 'getConfig');
configStub.withArgs('instreamTracking').returns(Object.assign(
{
enabled: true,
maxWindow: 10,
pollingFreq: 0
},
regex && {urlPattern: regex},
));
}

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 mockBidResponse(adUnit, requestId) {
const bid = {
'adUnitCod': adUnit.code,
'bidderCode': adUnit.bids[0].bidder,
'width': adUnit.sizes[0][0],
'height': adUnit.sizes[0][1],
'statusMessage': 'Bid available',
'adId': 'id',
'requestId': requestId,
'source': 'client',
'no_bid': false,
'cpm': '1.1495',
'ttl': 180,
'creativeId': 'id',
'netRevenue': true,
'currency': 'USD',
}
if (adUnit.mediaTypes.video) {
bid.videoCacheKey = VIDEO_CACHE_KEY;
}
return bid
}

function mockBidRequest(adUnit, bidResponse) {
return {
'bidderCode': bidResponse.bidderCode,
'auctionId': '20882439e3238c',
'bidderRequestId': 'bidderRequestId',
'bids': [
{
'adUnitCode': adUnit.code,
'mediaTypes': adUnit.mediaTypes,
'bidder': bidResponse.bidderCode,
'bidId': bidResponse.requestId,
'sizes': adUnit.sizes,
'params': adUnit.bids[0].params,
'bidderRequestId': 'bidderRequestId',
'auctionId': '20882439e3238c',
}
],
'auctionStart': 1505250713622,
'timeout': 3000
};
}

function getMockInput(mediaType) {
const bannerAdUnit = {
code: 'banner',
mediaTypes: {banner: {sizes: [[300, 250]]}},
sizes: [[300, 250]],
bids: [{bidder: BIDDER_CODE, params: {placementId: 'id'}}]
};
const outStreamAdUnit = {
code: 'video-' + OUTSTREAM,
mediaTypes: {video: {playerSize: [640, 480], context: OUTSTREAM}},
sizes: [[640, 480]],
bids: [{bidder: BIDDER_CODE, params: {placementId: 'id'}}]
};
const inStreamAdUnit = {
code: 'video-' + INSTREAM,
mediaTypes: {video: {playerSize: [640, 480], context: INSTREAM}},
sizes: [[640, 480]],
bids: [{bidder: BIDDER_CODE, params: {placementId: 'id'}}]
};

let adUnit;
switch (mediaType) {
default:
case 'banner':
adUnit = bannerAdUnit;
break;
case OUTSTREAM:
adUnit = outStreamAdUnit;
break;
case INSTREAM:
adUnit = inStreamAdUnit;
break;
}

const bidResponse = mockBidResponse(adUnit, utils.getUniqueIdentifierStr());
const bidderRequest = mockBidRequest(adUnit, bidResponse);
return {
adUnits: [adUnit],
bidsReceived: [bidResponse],
bidderRequests: [bidderRequest],
};
}

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({
adUnits: [],
bidsReceived: [],
bidderRequests: []
}), 'should not start tracking when tracking is disabled');
});

it('run only if instream bids are present', function () {
enableInstreamTracking();
assert.isNotOk(trackInstreamDeliveredImpressions({adUnits: [], bidsReceived: [], bidderRequests: []}));
});

it('checks for instream bids', function (done) {
enableInstreamTracking();
assert.isNotOk(trackInstreamDeliveredImpressions(getMockInput('banner')), 'should not start tracking when banner bids are present')
assert.isNotOk(trackInstreamDeliveredImpressions(getMockInput(OUTSTREAM)), 'should not start tracking when outstream bids are present')
mockPerformanceApi({});
assert.isOk(trackInstreamDeliveredImpressions(getMockInput(INSTREAM)), 'should start tracking when instream bids are present')
setTimeout(done, 10);
});
});

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

beforeEach(function () {
spyEventsOn = sandbox.spy(events, 'emit');
});

it('BID WON event is not emitted when no video cache key entries are present', function (done) {
enableInstreamTracking();
trackInstreamDeliveredImpressions(getMockInput(INSTREAM));
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) {
enableInstreamTracking();
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) {
enableInstreamTracking(/cache/);
const bidWonSpy = sandbox.spy();
events.on('bidWon', bidWonSpy);
mockPerformanceApi({adServerCallSent: true, videoPresent: true});

trackInstreamDeliveredImpressions(getMockInput(INSTREAM));
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 f2df223

Please sign in to comment.