Skip to content

Commit

Permalink
AdKernel adapter GDPR support
Browse files Browse the repository at this point in the history
Video ad unit size issue fix
  • Loading branch information
ckbo3hrk committed May 30, 2018
1 parent 71503a7 commit 8ff6fa5
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 57 deletions.
56 changes: 35 additions & 21 deletions modules/adkernelBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import includes from 'core-js/library/fn/array/includes';
const VIDEO_TARGETING = ['mimes', 'minduration', 'maxduration', 'protocols',
'startdelay', 'linearity', 'boxingallowed', 'playbackmethod', 'delivery',
'pos', 'api', 'ext'];
const VERSION = '1.2';
const VERSION = '1.3';

/**
* Adapter for requesting bids from AdKernel white-label display platform
Expand All @@ -21,23 +21,14 @@ export const spec = {
return 'params' in bidRequest && typeof bidRequest.params.host !== 'undefined' &&
'zoneId' in bidRequest.params && !isNaN(Number(bidRequest.params.zoneId));
},
buildRequests: function(bidRequests) {
let auctionId;
let dispatch = bidRequests.map(buildImp)
.reduce((acc, curr, index) => {
let bidRequest = bidRequests[index];
let zoneId = bidRequest.params.zoneId;
let host = bidRequest.params.host;
acc[host] = acc[host] || {};
acc[host][zoneId] = acc[host][zoneId] || [];
acc[host][zoneId].push(curr);
auctionId = bidRequest.bidderRequestId;
return acc;
}, {});
let requests = [];
Object.keys(dispatch).forEach(host => {
Object.keys(dispatch[host]).forEach(zoneId => {
const request = buildRtbRequest(dispatch[host][zoneId], auctionId);
buildRequests: function(bidRequests, bidderRequest) {
let impDispatch = dispatchImps(bidRequests);
const gdprConsent = bidderRequest.gdprConsent;
const auctionId = bidderRequest.auctionId;
const requests = [];
Object.keys(impDispatch).forEach(host => {
Object.keys(impDispatch[host]).forEach(zoneId => {
const request = buildRtbRequest(impDispatch[host][zoneId], auctionId, gdprConsent);
requests.push({
method: 'GET',
url: `${window.location.protocol}//${host}/rtbg`,
Expand Down Expand Up @@ -102,6 +93,22 @@ export const spec = {

registerBidder(spec);

/**
* Dispatch impressions by ad network host and zone
*/
function dispatchImps(bidRequests) {
return bidRequests.map(buildImp)
.reduce((acc, curr, index) => {
let bidRequest = bidRequests[index];
let zoneId = bidRequest.params.zoneId;
let host = bidRequest.params.host;
acc[host] = acc[host] || {};
acc[host][zoneId] = acc[host][zoneId] || [];
acc[host][zoneId].push(curr);
return acc;
}, {});
}

/**
* Builds parameters object for single impression
*/
Expand All @@ -113,13 +120,13 @@ function buildImp(bidRequest) {

if (bidRequest.mediaType === BANNER || utils.deepAccess(bidRequest, `mediaTypes.banner`) ||
(bidRequest.mediaTypes === undefined && bidRequest.mediaType === undefined)) {
let sizes = canonicalizeSizesArray(utils.deepAccess(bidRequest, `mediaTypes.banner.sizes`) || bidRequest.sizes);
let sizes = canonicalizeSizesArray(bidRequest.sizes);
imp.banner = {
format: sizes.map(s => ({'w': s[0], 'h': s[1]})),
topframe: 0
};
} else if (bidRequest.mediaType === VIDEO || utils.deepAccess(bidRequest, 'mediaTypes.video')) {
let size = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize') || canonicalizeSizesArray(bidRequest.sizes)[0];
let size = canonicalizeSizesArray(bidRequest.sizes)[0];
imp.video = {
w: size[0],
h: size[1]
Expand Down Expand Up @@ -152,8 +159,9 @@ function canonicalizeSizesArray(sizes) {
* Builds complete rtb request
* @param imps collection of impressions
* @param auctionId
* @param gdprConsent
*/
function buildRtbRequest(imps, auctionId) {
function buildRtbRequest(imps, auctionId, gdprConsent) {
let req = {
'id': auctionId,
'imp': imps,
Expand All @@ -172,6 +180,12 @@ function buildRtbRequest(imps, auctionId) {
if (utils.getDNT()) {
req.device.dnt = 1;
}
if (gdprConsent && gdprConsent.gdprApplies !== undefined) {
req.regs = {ext: {gdpr: Number(gdprConsent.gdprApplies)}};
}
if (gdprConsent && gdprConsent.consentString !== undefined) {
req.user = {ext: {consent: gdprConsent.consentString}};
}
return req;
}

Expand Down
109 changes: 73 additions & 36 deletions test/spec/modules/adkernelBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {expect} from 'chai';
import {spec} from 'modules/adkernelBidAdapter';
import * as utils from 'src/utils';
import {parse as parseUrl} from 'src/url';

describe('Adkernel adapter', () => {
const bid1_zone1 = {
Expand All @@ -14,7 +15,7 @@ describe('Adkernel adapter', () => {
bidId: 'Bid_02',
params: {zoneId: 2, host: 'rtb.adkernel.com'},
adUnitCode: 'ad-unit-2',
sizes: [[728, 90]]
sizes: [728, 90]
}, bid3_host2 = {
bidder: 'adkernel',
bidId: 'Bid_02',
Expand All @@ -41,14 +42,19 @@ describe('Adkernel adapter', () => {
sizes: [[728, 90]]
}, bid_video = {
bidder: 'adkernel',
transactionId: '866394b8-5d37-4d49-803e-f1bdb595f73e',
bidId: 'Bid_Video',
sizes: [640, 480],
mediaType: 'video',
bidderRequestId: '18b2a61ea5d9a7',
auctionId: 'de45acf1-9109-4e52-8013-f2b7cf5f6766',
sizes: [[640, 480]],
params: {
zoneId: 1,
host: 'rtb.adkernel.com',
host: 'rtb.adkernel.com'
},
mediaTypes: {
video: {
mimes: ['video/mp4', 'video/webm', 'video/x-flv']
context: 'instream',
playerSize: [[640, 480]]
}
},
adUnitCode: 'ad-unit-1'
Expand Down Expand Up @@ -107,6 +113,20 @@ describe('Adkernel adapter', () => {
}
};

function buildRequest(bidRequests, bidderRequest = {}, url = 'https://example.com/index.html', dnt = true) {
let wmock = sinon.stub(utils, 'getTopWindowLocation').callsFake(() => {
let loc = parseUrl(url);
loc.protocol += ':';
return loc;
});
let dntmock = sinon.stub(utils, 'getDNT').callsFake(() => dnt);
let pbRequests = spec.buildRequests(bidRequests, bidderRequest);
wmock.restore();
dntmock.restore();
let rtbRequests = pbRequests.map(r => JSON.parse(r.data.r));
return [pbRequests, rtbRequests];
}

describe('input parameters validation', () => {
it('empty request shouldn\'t generate exception', () => {
expect(spec.isBidRequestValid({
Expand All @@ -128,20 +148,10 @@ describe('Adkernel adapter', () => {
});

describe('banner request building', () => {
let bidRequest;
let bidRequest, bidRequests, _;
before(() => {
let wmock = sinon.stub(utils, 'getTopWindowLocation').callsFake(() => ({
protocol: 'https:',
hostname: 'example.com',
host: 'example.com',
pathname: '/index.html',
href: 'https://example.com/index.html'
}));
let dntmock = sinon.stub(utils, 'getDNT').callsFake(() => true);
let request = spec.buildRequests([bid1_zone1])[0];
bidRequest = JSON.parse(request.data.r);
wmock.restore();
dntmock.restore();
[_, bidRequests] = buildRequest([bid1_zone1]);
bidRequest = bidRequests[0];
});

it('should be a first-price auction', () => {
Expand Down Expand Up @@ -176,40 +186,67 @@ describe('Adkernel adapter', () => {
expect(bidRequest.device).to.have.property('ua', 'caller');
expect(bidRequest.device).to.have.property('dnt', 1);
});

it('shouldn\'t contain gdpr-related information for default request', () => {
let [_, bidRequests] = buildRequest([bid1_zone1]);
expect(bidRequests[0]).to.not.have.property('regs');
expect(bidRequests[0]).to.not.have.property('user');
});

it('should contain gdpr-related information if consent is configured', () => {
let [_, bidRequests] = buildRequest([bid1_zone1],
{gdprConsent: {gdprApplies: true, consentString: 'test-consent-string', vendorData: {}}});
let bidRequest = bidRequests[0];
expect(bidRequest).to.have.property('regs');
expect(bidRequest.regs.ext).to.be.eql({'gdpr': 1});
expect(bidRequest).to.have.property('user');
expect(bidRequest.user.ext).to.be.eql({'consent': 'test-consent-string'});
});

it('should\'t contain consent string if gdpr isn\'t applied', () => {
let [_, bidRequests] = buildRequest([bid1_zone1], {gdprConsent: {gdprApplies: false}});
let bidRequest = bidRequests[0];
expect(bidRequest).to.have.property('regs');
expect(bidRequest.regs.ext).to.be.eql({'gdpr': 0});
expect(bidRequest).to.not.have.property('user');
});

it('should\'t pass dnt if state is unknown', () => {
let [_, bidRequests] = buildRequest([bid1_zone1], {}, 'https://example.com/index.html', false);
expect(bidRequests[0].device).to.not.have.property('dnt');
});
});

describe('video request building', () => {
let bidRequest;

let _, bidRequests;
before(() => {
let request = spec.buildRequests([bid_video])[0];
bidRequest = JSON.parse(request.data.r);
[_, bidRequests] = buildRequest([bid_video]);
});

it('should have video object', () => {
expect(bidRequest.imp[0]).to.have.property('video');
expect(bidRequests[0].imp[0]).to.have.property('video');
});

it('should have h/w', () => {
expect(bidRequest.imp[0].video).to.have.property('w', 640);
expect(bidRequest.imp[0].video).to.have.property('h', 480);
expect(bidRequests[0].imp[0].video).to.have.property('w', 640);
expect(bidRequests[0].imp[0].video).to.have.property('h', 480);
});

it('should have tagid', () => {
expect(bidRequest.imp[0]).to.have.property('tagid', 'ad-unit-1');
expect(bidRequests[0].imp[0]).to.have.property('tagid', 'ad-unit-1');
});
});

describe('requests routing', () => {
it('should issue a request for each host', () => {
let pbRequests = spec.buildRequests([bid1_zone1, bid3_host2]);
let [pbRequests, _] = buildRequest([bid1_zone1, bid3_host2]);
expect(pbRequests).to.have.length(2);
expect(pbRequests[0].url).to.have.string(`//${bid1_zone1.params.host}/`);
expect(pbRequests[1].url).to.have.string(`//${bid3_host2.params.host}/`);
});

it('should issue a request for each zone', () => {
let pbRequests = spec.buildRequests([bid1_zone1, bid2_zone2]);
let [pbRequests, _] = buildRequest([bid1_zone1, bid2_zone2]);
expect(pbRequests).to.have.length(2);
expect(pbRequests[0].data.zone).to.be.equal(bid1_zone1.params.zoneId);
expect(pbRequests[1].data.zone).to.be.equal(bid2_zone2.params.zoneId);
Expand All @@ -218,8 +255,8 @@ describe('Adkernel adapter', () => {

describe('responses processing', () => {
it('should return fully-initialized banner bid-response', () => {
let request = spec.buildRequests([bid1_zone1])[0];
let resp = spec.interpretResponse({body: bidResponse1}, request)[0];
let [pbRequests, _] = buildRequest([bid1_zone1]);
let resp = spec.interpretResponse({body: bidResponse1}, pbRequests[0])[0];
expect(resp).to.have.property('requestId', 'Bid_01');
expect(resp).to.have.property('cpm', 3.01);
expect(resp).to.have.property('width', 300);
Expand All @@ -233,8 +270,8 @@ describe('Adkernel adapter', () => {
});

it('should return fully-initialized video bid-response', () => {
let request = spec.buildRequests([bid_video])[0];
let resp = spec.interpretResponse({body: videoBidResponse}, request)[0];
let [pbRequests, _] = buildRequest([bid_video]);
let resp = spec.interpretResponse({body: videoBidResponse}, pbRequests[0])[0];
expect(resp).to.have.property('requestId', 'Bid_Video');
expect(resp.mediaType).to.equal('video');
expect(resp.cpm).to.equal(0.00145);
Expand All @@ -244,15 +281,15 @@ describe('Adkernel adapter', () => {
});

it('should add nurl as pixel for banner response', () => {
let request = spec.buildRequests([bid1_zone1])[0];
let resp = spec.interpretResponse({body: bidResponse1}, request)[0];
let [pbRequests, _] = buildRequest([bid1_zone1]);
let resp = spec.interpretResponse({body: bidResponse1}, pbRequests[0])[0];
let expectedNurl = bidResponse1.seatbid[0].bid[0].nurl + '&px=1';
expect(resp.ad).to.have.string(expectedNurl);
});

it('should handle bidresponse with user-sync only', () => {
let request = spec.buildRequests([bid1_zone1])[0];
let resp = spec.interpretResponse({body: usersyncOnlyResponse}, request);
let [pbRequests, _] = buildRequest([bid1_zone1]);
let resp = spec.interpretResponse({body: usersyncOnlyResponse}, pbRequests[0]);
expect(resp).to.have.length(0);
});

Expand Down

0 comments on commit 8ff6fa5

Please sign in to comment.