Skip to content

Commit

Permalink
Add Publisher Common ID module (#2150)
Browse files Browse the repository at this point in the history
* Add Publisher Common ID module

* Specify base 10 for parseInt in PubCommonID
  • Loading branch information
pycnvr authored and harpere committed Feb 20, 2018
1 parent 2d1d0e0 commit c6f448b
Show file tree
Hide file tree
Showing 4 changed files with 317 additions and 1 deletion.
15 changes: 14 additions & 1 deletion modules/conversantBidAdapter.js
Expand Up @@ -5,7 +5,7 @@ import { BANNER, VIDEO } from 'src/mediaTypes';
const BIDDER_CODE = 'conversant';
const URL = '//media.msg.dotomi.com/s2s/header/24';
const SYNC_URL = '//media.msg.dotomi.com/w/user.sync';
const VERSION = '2.2.1';
const VERSION = '2.2.2';

export const spec = {
code: BIDDER_CODE,
Expand Down Expand Up @@ -54,6 +54,7 @@ export const spec = {
const isPageSecure = (loc.protocol === 'https:') ? 1 : 0;
let siteId = '';
let requestId = '';
let pubcid = null;

const conversantImps = validBidRequests.map(function(bid) {
const bidfloor = utils.getBidIdParameter('bidfloor', bid.params);
Expand Down Expand Up @@ -95,6 +96,10 @@ export const spec = {
imp.banner = banner;
}

if (bid.crumbs && bid.crumbs.pubcid) {
pubcid = bid.crumbs.pubcid;
}

return imp;
});

Expand All @@ -110,6 +115,14 @@ export const spec = {
at: 1
};

if (pubcid) {
payload.user = {
ext: {
fpc: pubcid
}
};
}

return {
method: 'POST',
url: URL,
Expand Down
100 changes: 100 additions & 0 deletions modules/pubCommonId.js
@@ -0,0 +1,100 @@
/**
* This modules adds Publisher Common ID support to prebid.js. It's a simple numeric id
* stored in the page's domain. When the module is included, an id is generated if needed,
* persisted as a cookie, and automatically appended to all the bidRequest as bid.crumbs.pubcid.
*/
import * as utils from 'src/utils'
import { config } from 'src/config';

const COOKIE_NAME = '_pubcid';
const DEFAULT_EXPIRES = 2628000; // 5-year worth of minutes
const PUB_COMMON = 'PublisherCommonId';

var pubcidEnabled = true;
var interval = DEFAULT_EXPIRES;

export function isPubcidEnabled() { return pubcidEnabled; }
export function getExpInterval() { return interval; }

/**
* Decorate ad units with pubcid. This hook function is called before the
* real pbjs.requestBids is invoked, and can modify its parameter. The cookie is
* not updated until this function is called.
* @param {Object} config This is the same parameter as pbjs.requestBids, and config.adUnits will be updated.
* @param {function} next The next function in the chain
*/

export function requestBidHook(config, next) {
let adUnits = config.adUnits || $$PREBID_GLOBAL$$.adUnits;
let pubcid = null;

// Pass control to the next function if not enabled
if (!pubcidEnabled) {
return next.apply(this, arguments);
}

if (typeof window[PUB_COMMON] === 'object') {
// If the page includes its own pubcid object, then use that instead.
pubcid = window[PUB_COMMON].getId();
utils.logMessage(PUB_COMMON + ': pubcid = ' + pubcid);
} else {
// Otherwise get the existing cookie or create a new id
pubcid = getCookie(COOKIE_NAME) || utils.generateUUID();

// Update the cookie with the latest expiration date
setCookie(COOKIE_NAME, pubcid, interval);
utils.logMessage('pbjs: pubcid = ' + pubcid);
}

// Append pubcid to each bid object, which will be incorporated
// into bid requests later.
if (adUnits && pubcid) {
adUnits.forEach((unit) => {
unit.bids.forEach((bid) => {
Object.assign(bid, {crumbs: {pubcid}});
});
});
}
return next.apply(this, arguments);
}

// Helper to set a cookie
export function setCookie(name, value, expires) {
let expTime = new Date();
expTime.setTime(expTime.getTime() + expires * 1000 * 60);
window.document.cookie = name + '=' + encodeURIComponent(value) + ';path=/;expires=' +
expTime.toGMTString();
}

// Helper to read a cookie
export function getCookie(name) {
let m = window.document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]*)\\s*(;|$)');
return m ? decodeURIComponent(m[2]) : null;
}

/**
* Configuration function
* @param {boolean} enable Enable or disable pubcid. By default the module is enabled.
* @param {number} expInterval Expiration interval of the cookie in minutes.
*/

export function setConfig({ enable = true, expInterval = DEFAULT_EXPIRES } = {}) {
pubcidEnabled = enable;
interval = parseInt(expInterval, 10);
if (isNaN(interval)) {
interval = DEFAULT_EXPIRES;
}
}

/**
* Initialize module by 1) subscribe to configuration changes and 2) register hook
*/
export function initPubcid() {
config.getConfig('pubcid', config => setConfig(config.pubcid));

if (utils.cookiesAreEnabled()) {
$$PREBID_GLOBAL$$.requestBids.addHook(requestBidHook);
}
}

initPubcid();
15 changes: 15 additions & 0 deletions test/spec/modules/conversantBidAdapter_spec.js
Expand Up @@ -227,6 +227,8 @@ describe('Conversant adapter tests', function() {
expect(payload.device).to.have.property('h', screen.height);
expect(payload.device).to.have.property('dnt').that.is.oneOf([0, 1]);
expect(payload.device).to.have.property('ua', navigator.userAgent);

expect(payload).to.not.have.property('user'); // there should be no user by default
});

it('Verify interpretResponse', function() {
Expand Down Expand Up @@ -279,4 +281,17 @@ describe('Conversant adapter tests', function() {
response = spec.interpretResponse({id: '123', seatbid: []}, {});
expect(response).to.be.an('array').with.lengthOf(0);
});

it('Verify publisher commond id support', function() {
// clone bidRequests
let requests = utils.deepClone(bidRequests)

// add pubcid to every entry
requests.forEach((unit) => {
Object.assign(unit, {crumbs: {pubcid: 12345}});
});
// construct http post payload
const payload = spec.buildRequests(requests).data;
expect(payload).to.have.deep.property('user.ext.fpc', 12345);
});
})
188 changes: 188 additions & 0 deletions test/spec/modules/pubCommonId_spec.js
@@ -0,0 +1,188 @@
import {
requestBidHook,
getCookie,
setCookie,
setConfig,
isPubcidEnabled,
getExpInterval,
initPubcid } from 'modules/pubCommonId';
import { getAdUnits } from 'test/fixtures/fixtures';
import * as auctionModule from 'src/auction';
import { registerBidder } from 'src/adapters/bidderFactory';
import * as utils from 'src/utils';

var assert = require('chai').assert;
var expect = require('chai').expect;

const COOKIE_NAME = '_pubcid';
const TIMEOUT = 2000;

describe('Publisher Common ID', function () {
describe('Decorate adUnits', function () {
it('Check same cookie', function () {
let adUnits1 = getAdUnits();
let adUnits2 = getAdUnits();
let innerAdUnits1;
let innerAdUnits2;
let pubcid = getCookie(COOKIE_NAME);

expect(pubcid).to.be.null; // there should be no cookie initially

requestBidHook({adUnits: adUnits1}, (config) => { innerAdUnits1 = config.adUnits });
pubcid = getCookie(COOKIE_NAME); // cookies is created after requestbidHook

innerAdUnits1.forEach((unit) => {
unit.bids.forEach((bid) => {
expect(bid).to.have.deep.property('crumbs.pubcid');
expect(bid.crumbs.pubcid).to.equal(pubcid);
});
});
requestBidHook({adUnits: adUnits2}, (config) => { innerAdUnits2 = config.adUnits });
assert.deepEqual(innerAdUnits1, innerAdUnits2);
});

it('Check different cookies', function () {
let adUnits1 = getAdUnits();
let adUnits2 = getAdUnits();
let innerAdUnits1;
let innerAdUnits2;
let pubcid1;
let pubcid2;

requestBidHook({adUnits: adUnits1}, (config) => { innerAdUnits1 = config.adUnits });
pubcid1 = getCookie(COOKIE_NAME); // get first cookie
setCookie(COOKIE_NAME, '', -1); // erase cookie

innerAdUnits1.forEach((unit) => {
unit.bids.forEach((bid) => {
expect(bid).to.have.deep.property('crumbs.pubcid');
expect(bid.crumbs.pubcid).to.equal(pubcid1);
});
});

requestBidHook({adUnits: adUnits2}, (config) => { innerAdUnits2 = config.adUnits });
pubcid2 = getCookie(COOKIE_NAME); // get second cookie

innerAdUnits2.forEach((unit) => {
unit.bids.forEach((bid) => {
expect(bid).to.have.deep.property('crumbs.pubcid');
expect(bid.crumbs.pubcid).to.equal(pubcid2);
});
});

expect(pubcid1).to.not.equal(pubcid2);
});

it('Check new cookie', function () {
let adUnits = getAdUnits();
let innerAdUnits;
let pubcid = utils.generateUUID();

setCookie(COOKIE_NAME, pubcid, 600);
requestBidHook({adUnits}, (config) => { innerAdUnits = config.adUnits });
innerAdUnits.forEach((unit) => {
unit.bids.forEach((bid) => {
expect(bid).to.have.deep.property('crumbs.pubcid');
expect(bid.crumbs.pubcid).to.equal(pubcid);
});
});
});
});

describe('Configuration', function () {
it('empty config', function () {
// this should work as usual
setConfig({});
let adUnits = getAdUnits();
let innerAdUnits;
requestBidHook({adUnits}, (config) => { innerAdUnits = config.adUnits });
let pubcid = getCookie(COOKIE_NAME);
innerAdUnits.forEach((unit) => {
unit.bids.forEach((bid) => {
expect(bid).to.have.deep.property('crumbs.pubcid');
expect(bid.crumbs.pubcid).to.equal(pubcid);
});
});
});

it('disable', function () {
setConfig({enable: false});
setCookie(COOKIE_NAME, '', -1); // erase cookie
let adUnits = getAdUnits();
let unmodified = getAdUnits();
let innerAdUnits;
expect(isPubcidEnabled()).to.be.false;
requestBidHook({adUnits}, (config) => { innerAdUnits = config.adUnits });
expect(getCookie(COOKIE_NAME)).to.be.null;
assert.deepEqual(innerAdUnits, unmodified);
setConfig({enable: true}); // reset
requestBidHook({adUnits}, (config) => { innerAdUnits = config.adUnits });
innerAdUnits.forEach((unit) => {
unit.bids.forEach((bid) => {
expect(bid).to.have.deep.property('crumbs.pubcid');
});
});
});

it('change expiration time', function () {
setConfig({expInterval: 100});
setCookie(COOKIE_NAME, '', -1); // erase cookie
expect(getExpInterval()).to.equal(100);
let adUnits = getAdUnits();
let innerAdUnits;
requestBidHook({adUnits}, (config) => { innerAdUnits = config.adUnits });
innerAdUnits.every((unit) => {
unit.bids.forEach((bid) => {
expect(bid).to.have.deep.property('crumbs.pubcid');
});
})
});
});

describe('Invoking requestBid', function () {
let createAuctionStub;
let adUnits;
let adUnitCodes;
let capturedReqs;
let sampleSpec = {
code: 'sampleBidder',
isBidRequestValid: () => {},
buildRequest: (reqs) => {},
interpretResponse: () => {},
getUserSyncs: () => {}
};

beforeEach(() => {
adUnits = [{
code: 'adUnit-code',
mediaTypes: {
banner: {},
native: {},
},
sizes: [[300, 200], [300, 600]],
bids: [
{bidder: 'sampleBidder', params: {placementId: 'banner-only-bidder'}}
]
}];
adUnitCodes = ['adUnit-code'];
let auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: TIMEOUT});
createAuctionStub = sinon.stub(auctionModule, 'newAuction');
createAuctionStub.returns(auction);
initPubcid();
registerBidder(sampleSpec);
});

afterEach(() => {
auctionModule.newAuction.restore();
});

it('test hook', function() {
$$PREBID_GLOBAL$$.requestBids({adUnits});
adUnits.forEach((unit) => {
unit.bids.forEach((bid) => {
expect(bid).to.have.deep.property('crumbs.pubcid');
});
});
});
});
});

0 comments on commit c6f448b

Please sign in to comment.