Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auction key limiter feature #3825

Merged
merged 4 commits into from
May 28, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 89 additions & 2 deletions src/targeting.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { uniques, isGptPubadsDefined, getHighestCpm, getOldestHighestCpmBid, groupBy, isAdUnitCodeMatchingSlot, timestamp, deepAccess } from './utils';
import { uniques, isGptPubadsDefined, getHighestCpm, getOldestHighestCpmBid, groupBy, isAdUnitCodeMatchingSlot, timestamp, deepAccess, deepClone, logError, logWarn, logInfo } from './utils';
import { config } from './config';
import { NATIVE_TARGETING_KEYS } from './native';
import { auctionManager } from './auctionManager';
Expand Down Expand Up @@ -43,6 +43,40 @@ export function getHighestCpmBidsFromBidPool(bidsReceived, highestCpmCallback) {
return bids;
}

/**
* A descending sort function that will sort the list of objects based on the following two dimensions:
* - bids with a deal are sorted before bids w/o a deal
* - then sort bids in each grouping based on the hb_pb value
* eg: the following list of bids would be sorted like:
* [{
* "hb_adid": "vwx",
* "hb_pb": "28",
* "hb_deal": "7747"
* }, {
* "hb_adid": "jkl",
* "hb_pb": "10",
* "hb_deal": "9234"
* }, {
* "hb_adid": "stu",
* "hb_pb": "50"
* }, {
* "hb_adid": "def",
* "hb_pb": "2"
* }]
*/
export function sortByDealAndPriceBucket(a, b) {
if (a.adUnitTargeting.hb_deal !== undefined && b.adUnitTargeting.hb_deal === undefined) {
return -1;
}

if ((a.adUnitTargeting.hb_deal === undefined && b.adUnitTargeting.hb_deal !== undefined)) {
return 1;
}

// assuming both values either have a deal or don't have a deal - sort by the hb_pb param
return b.adUnitTargeting.hb_pb - a.adUnitTargeting.hb_pb;
}

/**
* @typedef {Object.<string,string>} targeting
* @property {string} targeting_key
Expand Down Expand Up @@ -122,8 +156,13 @@ export function newTargeting(auctionManager) {

targeting = flattenTargeting(targeting);

// make sure at least there is a entry per adUnit code in the targetingSet so receivers of SET_TARGETING call's can know what ad units are being invoked
const auctionKeysThreshold = config.getConfig('targetingControls.auctionKeysCharacterThreshold');
if (auctionKeysThreshold) {
logInfo(`Detected 'targetingControls.auctionKeysCharacterThreshold' was active for this auction; set with a limit of ${auctionKeysThreshold} characters. Running checks on auction keys...`);
targeting = filterTargetingKeys(targeting, auctionKeysThreshold);
}

// make sure at least there is a entry per adUnit code in the targetingSet so receivers of SET_TARGETING call's can know what ad units are being invoked
adUnitCodes.forEach(code => {
if (!targeting[code]) {
targeting[code] = {};
Expand All @@ -133,6 +172,54 @@ export function newTargeting(auctionManager) {
return targeting;
};

// create an encoded string variant based on the keypairs of the provided object
// - note this will encode the characters between the keys (ie = and &)
function convertKeysToQueryForm(keyMap) {
return Object.keys(keyMap).reduce(function (queryString, key) {
let encodedKeyPair = `${key}%3d${encodeURIComponent(keyMap[key])}%26`;
return queryString += encodedKeyPair;
}, '');
}

function filterTargetingKeys(targeting, auctionKeysThreshold) {
// read each targeting.adUnit object and sort the adUnits into a list of adUnitCodes based on priorization setting (eg CPM)
let targetingCopy = deepClone(targeting);

let targetingMap = Object.keys(targetingCopy).map(adUnitCode => {
return {
adUnitCode,
adUnitTargeting: targetingCopy[adUnitCode]
};
}).sort(sortByDealAndPriceBucket);

// iterate through the targeting based on above list and transform the keys into the query-equivalent and count characters
return targetingMap.reduce(function (accMap, currMap, index, arr) {
let adUnitQueryString = convertKeysToQueryForm(currMap.adUnitTargeting);

// for the last adUnit - trim last encoded ampersand from the converted query string
if ((index + 1) === arr.length) {
adUnitQueryString = adUnitQueryString.slice(0, -3);
}

// if under running threshold add to result
let code = currMap.adUnitCode;
let querySize = adUnitQueryString.length;
if (querySize <= auctionKeysThreshold) {
auctionKeysThreshold -= querySize;
logInfo(`AdUnit '${code}' auction keys comprised of ${querySize} characters. Deducted from running threshold; new limit is ${auctionKeysThreshold}`, targetingCopy[code]);

accMap[code] = targetingCopy[code];
} else {
logWarn(`The following keys for adUnitCode '${code}' exceeded the current limit of the 'auctionKeysCharacterThreshold' setting.\nThe key-set size was ${querySize}, the current allotted amount was ${auctionKeysThreshold}.\n`, targetingCopy[code]);
}

if ((index + 1) === arr.length && Object.keys(accMap).length === 0) {
logError('No auction targeting keys were permitted due to the setting in setConfig(targetingControls.auctionKeysCharacterThreshold). Please review setup and consider adjusting.');
}
return accMap;
}, {});
}

/**
* Converts targeting array and flattens to make it easily iteratable
* e.g: Sample input to this function
Expand Down
160 changes: 159 additions & 1 deletion test/spec/unit/core/targeting_spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { targeting as targetingInstance, filters } from 'src/targeting';
import { targeting as targetingInstance, filters, sortByDealAndPriceBucket } from 'src/targeting';
import { config } from 'src/config';
import { getAdUnits, createBidReceived } from 'test/fixtures/fixtures';
import CONSTANTS from 'src/constants.json';
Expand Down Expand Up @@ -128,6 +128,8 @@ describe('targeting tests', function () {
let amBidsReceivedStub;
let amGetAdUnitsStub;
let bidExpiryStub;
let logWarnStub;
let logErrorStub;
let bidsReceived;

beforeEach(function () {
Expand All @@ -140,6 +142,14 @@ describe('targeting tests', function () {
return ['/123456/header-bid-tag-0'];
});
bidExpiryStub = sandbox.stub(filters, 'isBidNotExpired').returns(true);
logWarnStub = sinon.stub(utils, 'logWarn');
logErrorStub = sinon.stub(utils, 'logError');
});

afterEach(function() {
config.resetConfig();
logWarnStub.restore();
logErrorStub.restore();
});

describe('when hb_deal is present in bid.adserverTargeting', function () {
Expand All @@ -165,6 +175,34 @@ describe('targeting tests', function () {
});
});

it('will enforce a limit on the number of auction keys when auctionKeysCharacterThreshold setting is active', function () {
config.setConfig({
targetingControls: {
auctionKeysCharacterThreshold: 150
}
});

const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0', '/123456/header-bid-tag-1']);
expect(targeting['/123456/header-bid-tag-1']).to.deep.equal({});
expect(targeting['/123456/header-bid-tag-0']).to.contain.keys('hb_pb', 'hb_adid', 'hb_bidder', 'hb_deal');
expect(targeting['/123456/header-bid-tag-0']['hb_adid']).to.equal(bid1.adId);
expect(logWarnStub.calledOnce).to.be.true;
});

it('will return an error when auctionKeysCharacterThreshold setting is set too low for any auction keys to be allowed', function () {
config.setConfig({
targetingControls: {
auctionKeysCharacterThreshold: 50
}
});

const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0', '/123456/header-bid-tag-1']);
expect(targeting['/123456/header-bid-tag-1']).to.deep.equal({});
expect(targeting['/123456/header-bid-tag-0']).to.deep.equal({});
expect(logWarnStub.calledTwice).to.be.true;
expect(logErrorStub.calledOnce).to.be.true;
});

it('selects the top bid when enableSendAllBids true', function () {
enableSendAllBids = true;
let targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']);
Expand Down Expand Up @@ -327,6 +365,126 @@ describe('targeting tests', function () {
});
});

describe('sortByDealAndPriceBucket', function() {
it('will properly sort bids when some bids have deals and some do not', function () {
let bids = [{
adUnitTargeting: {
hb_adid: 'abc',
hb_pb: '1.00',
hb_deal: '1234'
}
}, {
adUnitTargeting: {
hb_adid: 'def',
hb_pb: '0.50',
}
}, {
adUnitTargeting: {
hb_adid: 'ghi',
hb_pb: '20.00',
hb_deal: '4532'
}
}, {
adUnitTargeting: {
hb_adid: 'jkl',
hb_pb: '9.00',
hb_deal: '9864'
}
}, {
adUnitTargeting: {
hb_adid: 'mno',
hb_pb: '50.00',
}
}, {
adUnitTargeting: {
hb_adid: 'pqr',
hb_pb: '100.00',
}
}];
bids.sort(sortByDealAndPriceBucket);
expect(bids[0].adUnitTargeting.hb_adid).to.equal('ghi');
expect(bids[1].adUnitTargeting.hb_adid).to.equal('jkl');
expect(bids[2].adUnitTargeting.hb_adid).to.equal('abc');
expect(bids[3].adUnitTargeting.hb_adid).to.equal('pqr');
expect(bids[4].adUnitTargeting.hb_adid).to.equal('mno');
expect(bids[5].adUnitTargeting.hb_adid).to.equal('def');
});

it('will properly sort bids when all bids have deals', function () {
let bids = [{
adUnitTargeting: {
hb_adid: 'abc',
hb_pb: '1.00',
hb_deal: '1234'
}
}, {
adUnitTargeting: {
hb_adid: 'def',
hb_pb: '0.50',
hb_deal: '4321'
}
}, {
adUnitTargeting: {
hb_adid: 'ghi',
hb_pb: '2.50',
hb_deal: '4532'
}
}, {
adUnitTargeting: {
hb_adid: 'jkl',
hb_pb: '2.00',
hb_deal: '9864'
}
}];
bids.sort(sortByDealAndPriceBucket);
expect(bids[0].adUnitTargeting.hb_adid).to.equal('ghi');
expect(bids[1].adUnitTargeting.hb_adid).to.equal('jkl');
expect(bids[2].adUnitTargeting.hb_adid).to.equal('abc');
expect(bids[3].adUnitTargeting.hb_adid).to.equal('def');
});

it('will properly sort bids when no bids have deals', function () {
let bids = [{
adUnitTargeting: {
hb_adid: 'abc',
hb_pb: '1.00'
}
}, {
adUnitTargeting: {
hb_adid: 'def',
hb_pb: '0.10'
}
}, {
adUnitTargeting: {
hb_adid: 'ghi',
hb_pb: '10.00'
}
}, {
adUnitTargeting: {
hb_adid: 'jkl',
hb_pb: '10.01'
}
}, {
adUnitTargeting: {
hb_adid: 'mno',
hb_pb: '1.00'
}
}, {
adUnitTargeting: {
hb_adid: 'pqr',
hb_pb: '100.00'
}
}];
bids.sort(sortByDealAndPriceBucket);
expect(bids[0].adUnitTargeting.hb_adid).to.equal('pqr');
expect(bids[1].adUnitTargeting.hb_adid).to.equal('jkl');
expect(bids[2].adUnitTargeting.hb_adid).to.equal('ghi');
expect(bids[3].adUnitTargeting.hb_adid).to.equal('abc');
expect(bids[4].adUnitTargeting.hb_adid).to.equal('mno');
expect(bids[5].adUnitTargeting.hb_adid).to.equal('def');
});
});

describe('setTargetingForAst', function () {
let sandbox,
apnTagStub;
Expand Down