From ace59039b7a122646b084384f7e650d505f03043 Mon Sep 17 00:00:00 2001 From: kat Date: Thu, 8 Feb 2018 13:13:19 -0500 Subject: [PATCH] Add adapter for IAS (#2056) * PET-201: got a working version * PET-201: encoded query string * PET-201: added unit tests * PET-201: added missing keyword * PET-201: corrected coding styles * PET-201: decreased cpm so real bidders could win per code review --- modules/iasBidAdapter.js | 117 +++++++++++++++ modules/iasBidAdapter.md | 30 ++++ test/spec/modules/iasBidAdapter_spec.js | 190 ++++++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 modules/iasBidAdapter.js create mode 100644 modules/iasBidAdapter.md create mode 100644 test/spec/modules/iasBidAdapter_spec.js diff --git a/modules/iasBidAdapter.js b/modules/iasBidAdapter.js new file mode 100644 index 00000000000..61ba909527a --- /dev/null +++ b/modules/iasBidAdapter.js @@ -0,0 +1,117 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'ias'; + +function isBidRequestValid(bid) { + const { pubId, adUnitPath } = bid.params; + return !!(pubId && adUnitPath); +} + +/** + * Converts GPT-style size array into a string + * @param {Array} sizes: list of GPT-style sizes, e.g. [[300, 250], [300, 300]] + * @return {String} a string containing sizes, e.g. '[300.250,300.300]' + */ +function stringifySlotSizes(sizes) { + let result = ''; + if (utils.isArray(sizes)) { + result = sizes.reduce((acc, size) => { + acc.push(size.join('.')); + return acc; + }, []); + result = '[' + result.join(',') + ']'; + } + return result; +} + +function stringifySlot(bidRequest) { + const id = bidRequest.adUnitCode; + const ss = stringifySlotSizes(bidRequest.sizes); + const p = bidRequest.params.adUnitPath; + const slot = { id, ss, p }; + const keyValues = utils.getKeys(slot).map(function(key) { + return [key, slot[key]].join(':'); + }); + return '{' + keyValues.join(',') + '}'; +} + +function stringifyWindowSize() { + return [window.innerWidth || -1, window.innerHeight || -1].join('.'); +} + +function stringifyScreenSize() { + return [(window.screen && window.screen.width) || -1, (window.screen && window.screen.height) || -1].join('.'); +} + +function buildRequests(bidRequests) { + const IAS_HOST = '//pixel.adsafeprotected.com/services/pub'; + const anId = bidRequests[0].params.pubId; + + let queries = []; + queries.push(['anId', anId]); + queries = queries.concat(bidRequests.reduce(function(acc, request) { + acc.push(['slot', stringifySlot(request)]); + return acc; + }, [])); + + queries.push(['wr', stringifyWindowSize()]); + queries.push(['sr', stringifyScreenSize()]); + + const queryString = encodeURI(queries.map(qs => qs.join('=')).join('&')); + + return { + method: 'GET', + url: IAS_HOST, + data: queryString, + bidRequest: bidRequests[0] + } +} + +function getPageLevelKeywords(response) { + let result = {}; + shallowMerge(result, response.brandSafety); + result.fr = response.fr; + return result; +} + +function shallowMerge(dest, src) { + utils.getKeys(src).reduce((dest, srcKey) => { + dest[srcKey] = src[srcKey]; + return dest; + }, dest); +} + +function interpretResponse(serverResponse, request) { + const iasResponse = serverResponse.body; + const bidResponses = []; + + // Keys in common bid response are not used; + // Necessary to get around with prebid's common bid response check + const commonBidResponse = { + requestId: request.bidRequest.bidId, + cpm: 0.01, + width: 100, + height: 200, + creativeId: 434, + dealId: 42, + currency: 'usd', + netRevenue: true, + ttl: 360 + }; + + shallowMerge(commonBidResponse, getPageLevelKeywords(iasResponse)); + commonBidResponse.slots = iasResponse.slots; + bidResponses.push(commonBidResponse); + return bidResponses; +} + +export const spec = { + code: BIDDER_CODE, + aliases: [], + isBidRequestValid: isBidRequestValid, + buildRequests: buildRequests, + interpretResponse: interpretResponse +}; + +registerBidder(spec); diff --git a/modules/iasBidAdapter.md b/modules/iasBidAdapter.md new file mode 100644 index 00000000000..3224fbf4a26 --- /dev/null +++ b/modules/iasBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +``` +Module Name: Integral Ad Science(IAS) Bidder Adapter +Module Type: Bidder Adapter +Maintainer: kat@integralads.com +``` + +# Description + +This module is an integration with prebid.js with an IAS product, pet.js. It is not a bidder per se but works in a similar way: retrieve data that publishers might be interested in setting keyword targeting. + +# Test Parameters +``` + var adUnits = [ + { + code: 'ias-dfp-test-async', + sizes: [[300, 250]], // a display size + bids: [ + { + bidder: "ias", + params: { + pubId: '99', + adUnitPath: '/57514611/news.com' + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/iasBidAdapter_spec.js b/test/spec/modules/iasBidAdapter_spec.js new file mode 100644 index 00000000000..4f335ab22ba --- /dev/null +++ b/test/spec/modules/iasBidAdapter_spec.js @@ -0,0 +1,190 @@ +import { expect } from 'chai'; +import { spec } from 'modules/iasBidAdapter'; + +describe('iasBidAdapter is an adapter that', () => { + it('has the correct bidder code', () => { + expect(spec.code).to.equal('ias'); + }); + describe('has a method `isBidRequestValid` that', () => { + it('exists', () => { + expect(spec.isBidRequestValid).to.be.a('function'); + }); + it('returns false if bid params misses `pubId`', () => { + expect(spec.isBidRequestValid( + { + params: { + adUnitPath: 'someAdUnitPath' + } + })).to.equal(false); + }); + it('returns false if bid params misses `adUnitPath`', () => { + expect(spec.isBidRequestValid( + { + params: { + pubId: 'somePubId' + } + })).to.equal(false); + }); + it('returns true otherwise', () => { + expect(spec.isBidRequestValid( + { + params: { + adUnitPath: 'someAdUnitPath', + pubId: 'somePubId', + someOtherParam: 'abc' + } + })).to.equal(true); + }); + }); + + describe('has a method `buildRequests` that', () => { + it('exists', () => { + expect(spec.buildRequests).to.be.a('function'); + }); + describe('given bid requests, returns a `ServerRequest` instance that', () => { + let bidRequests, IAS_HOST; + beforeEach(() => { + IAS_HOST = '//pixel.adsafeprotected.com/services/pub'; + bidRequests = [ + { + adUnitCode: 'one-div-id', + auctionId: 'someAuctionId', + bidId: 'someBidId', + bidder: 'ias', + bidderRequestId: 'someBidderRequestId', + params: { + pubId: '1234', + adUnitPath: '/a/b/c' + }, + sizes: [ + [10, 20], + [300, 400] + ], + transactionId: 'someTransactionId' + }, + { + adUnitCode: 'two-div-id', + auctionId: 'someAuctionId', + bidId: 'someBidId', + bidder: 'ias', + bidderRequestId: 'someBidderRequestId', + params: { + pubId: '1234', + adUnitPath: '/d/e/f' + }, + sizes: [ + [50, 60] + ], + transactionId: 'someTransactionId' + } + ]; + }); + it('has property `method` of `GET`', () => { + expect(spec.buildRequests(bidRequests)).to.deep.include({ + method: 'GET' + }); + }); + it('has property `url` to be the correct IAS endpoint', () => { + expect(spec.buildRequests(bidRequests)).to.deep.include({ + url: IAS_HOST + }); + }); + describe('has property `data` that is an encode query string containing information such as', () => { + let val; + const ANID_PARAM = 'anId'; + const SLOT_PARAM = 'slot'; + const SLOT_ID_PARAM = 'id'; + const SLOT_SIZE_PARAM = 'ss'; + const SLOT_AD_UNIT_PATH_PARAM = 'p'; + + beforeEach(() => val = decodeURI(spec.buildRequests(bidRequests).data)); + it('publisher id', () => { + expect(val).to.have.string(`${ANID_PARAM}=1234`); + }); + it('ad slot`s id, size and ad unit path', () => { + expect(val).to.have.string(`${SLOT_PARAM}={${SLOT_ID_PARAM}:one-div-id,${SLOT_SIZE_PARAM}:[10.20,300.400],${SLOT_AD_UNIT_PATH_PARAM}:/a/b/c}`); + expect(val).to.have.string(`${SLOT_PARAM}={${SLOT_ID_PARAM}:two-div-id,${SLOT_SIZE_PARAM}:[50.60],${SLOT_AD_UNIT_PATH_PARAM}:/d/e/f}`); + }); + it('window size', () => { + expect(val).to.match(/.*wr=[0-9]*\.[0-9]*/); + }); + it('screen size', () => { + expect(val).to.match(/.*sr=[0-9]*\.[0-9]*/); + }); + }); + it('has property `bidRequest` that is the first passed in bid request', () => { + expect(spec.buildRequests(bidRequests)).to.deep.include({ + bidRequest: bidRequests[0] + }); + }); + }); + }); + describe('has a method `interpretResponse` that', () => { + it('exists', () => { + expect(spec.interpretResponse).to.be.a('function'); + }); + describe('returns a list of bid response that', () => { + let bidResponse, slots; + beforeEach(() => { + const request = { + bidRequest: { + bidId: '102938' + } + }; + slots = {}; + slots['test-div-id'] = { + id: '1234', + vw: ['60', '70'] + }; + slots['test-div-id-two'] = { + id: '5678', + vw: ['80', '90'] + }; + const serverResponse = { + body: { + brandSafety: { + adt: 'adtVal', + alc: 'alcVal', + dlm: 'dlmVal', + drg: 'drgVal', + hat: 'hatVal', + off: 'offVal', + vio: 'vioVal' + }, + fr: 'false', + slots: slots + }, + headers: {} + }; + bidResponse = spec.interpretResponse(serverResponse, request); + }); + it('has IAS keyword `adt` as property', () => { + expect(bidResponse[0]).to.deep.include({ adt: 'adtVal' }); + }); + it('has IAS keyword `alc` as property', () => { + expect(bidResponse[0]).to.deep.include({ alc: 'alcVal' }); + }); + it('has IAS keyword `dlm` as property', () => { + expect(bidResponse[0]).to.deep.include({ dlm: 'dlmVal' }); + }); + it('has IAS keyword `drg` as property', () => { + expect(bidResponse[0]).to.deep.include({ drg: 'drgVal' }); + }); + it('has IAS keyword `hat` as property', () => { + expect(bidResponse[0]).to.deep.include({ hat: 'hatVal' }); + }); + it('has IAS keyword `off` as property', () => { + expect(bidResponse[0]).to.deep.include({ off: 'offVal' }); + }); + it('has IAS keyword `vio` as property', () => { + expect(bidResponse[0]).to.deep.include({ vio: 'vioVal' }); + }); + it('has IAS keyword `fr` as property', () => { + expect(bidResponse[0]).to.deep.include({ fr: 'false' }); + }); + it('has property `slots`', () => { + expect(bidResponse[0]).to.deep.include({ slots: slots }); + }); + }); + }); +});