From b78cfd996573adcfbd719502e17082a28387a7b0 Mon Sep 17 00:00:00 2001 From: ricardoe Date: Thu, 18 Jan 2018 13:27:02 -0600 Subject: [PATCH] GumGum Adapter for Prebid.js 1.0 (#1966) * GumGum Adapter for Prebid.js 1.0 * removed getUserSyncs. Give cpm a non-zero value when bidRequest is for test unit so DFP chooses it. * parsing slot ID as integer from params * ADSS-78 removed bidderCode from response. Correctly parsing bidRequest.sizes to account for 1-dimensional array. --- modules/gumgumBidAdapter.js | 320 ++++++++-------- modules/gumgumBidAdapter.md | 40 ++ test/spec/modules/gumgumBidAdapter_spec.js | 401 +++++++-------------- 3 files changed, 319 insertions(+), 442 deletions(-) create mode 100644 modules/gumgumBidAdapter.md diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 521dfabfb2d..2627b7417f5 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -1,176 +1,170 @@ -const bidfactory = require('src/bidfactory'); -const bidmanager = require('src/bidmanager'); -const utils = require('src/utils'); -const adloader = require('src/adloader'); -var adaptermanager = require('src/adaptermanager'); - -const BIDDER_CODE = 'gumgum'; -const CALLBACKS = {}; - -const GumgumAdapter = function GumgumAdapter() { - const bidEndpoint = `https://g2.gumgum.com/hbid/imp`; - - let topWindow; - let topScreen; - let pageViewId; - const requestCache = {}; - const throttleTable = {}; - const defaultThrottle = 3e4; - const dtCredentials = { member: 'YcXr87z2lpbB' }; +import * as utils from 'src/utils' + +import { config } from 'src/config' +import { registerBidder } from 'src/adapters/bidderFactory' + +const BIDDER_CODE = 'gumgum' +const ALIAS_BIDDER_CODE = ['gg'] +const BID_ENDPOINT = `https://g2.gumgum.com/hbid/imp` +const DT_CREDENTIALS = { member: 'YcXr87z2lpbB' } +const TIME_TO_LIVE = 60 +let browserParams = {}; + +// TODO: potential 0 values for browserParams sent to ad server +function _getBrowserParams() { + let topWindow + let topScreen + if (browserParams.vw) { + // we've already initialized browserParams, just return it. + return browserParams + } try { topWindow = global.top; topScreen = topWindow.screen; } catch (error) { - return utils.logError(error); - } - - function _getTimeStamp() { - return new Date().getTime(); - } - - function _getDigiTrustQueryParams() { - function getDigiTrustId () { - var digiTrustUser = (window.DigiTrust && window.DigiTrust.getUser) ? window.DigiTrust.getUser(dtCredentials) : {}; - return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || ''; - }; - - let digiTrustId = getDigiTrustId(); - // Verify there is an ID and this user has not opted out - if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) { - return {}; - } - return { - 'dt': digiTrustId.id - }; + utils.logError(error); + return browserParams } - function _callBids({ bids }) { - const browserParams = { - vw: topWindow.innerWidth, - vh: topWindow.innerHeight, - sw: topScreen.width, - sh: topScreen.height, - pu: topWindow.location.href, - ce: navigator.cookieEnabled, - dpr: topWindow.devicePixelRatio || 1 - }; - - utils._each(bids, bidRequest => { - const { bidId - , params = {} - , placementCode - } = bidRequest; - const timestamp = _getTimeStamp(); - const trackingId = params.inScreen; - const nativeId = params['native']; - const slotId = params.inSlot; - const bid = { tmax: $$PREBID_GLOBAL$$.cbTimeout }; - - /* slot/native ads need the placement id */ - switch (true) { - case !!(params.inImage): bid.pi = 1; break; - case !!(params.inScreen): bid.pi = 2; break; - case !!(params.inSlot): bid.pi = 3; break; - case !!(params['native']): bid.pi = 5; break; - default: return utils.logWarn( - `[GumGum] No product selected for the placement ${placementCode}` + - ', please check your implementation.' - ); - } - - /* throttle based on the latest request for this product */ - const productId = bid.pi; - const requestKey = productId + '|' + placementCode; - const throttle = throttleTable[productId]; - const latestRequest = requestCache[requestKey]; - if (latestRequest && throttle && (timestamp - latestRequest) < throttle) { - return utils.logWarn( - `[GumGum] The refreshes for "${placementCode}" with the params ` + - `${JSON.stringify(params)} should be at least ${throttle / 1e3}s apart.` - ); - } - /* update the last request */ - requestCache[requestKey] = timestamp; - - /* tracking id is required for in-image and in-screen */ - if (trackingId) bid.t = trackingId; - /* native ads require a native placement id */ - if (nativeId) bid.ni = nativeId; - /* slot ads require a slot id */ - if (slotId) bid.si = slotId; - - /* include the pageViewId, if any */ - if (pageViewId) bid.pv = pageViewId; - - const cachedBid = Object.assign({ - placementCode, - id: bidId - }, bid); - - const callback = { jsonp: `$$PREBID_GLOBAL$$.handleGumGumCB['${bidId}']` }; - CALLBACKS[bidId] = _handleGumGumResponse(cachedBid); - const query = Object.assign(callback, browserParams, bid, _getDigiTrustQueryParams()); - const bidCall = `${bidEndpoint}?${utils.parseQueryStringParameters(query)}`; - adloader.loadScript(bidCall); - }); + browserParams = { + vw: topWindow.innerWidth, + vh: topWindow.innerHeight, + sw: topScreen.width, + sh: topScreen.height, + pu: utils.getTopWindowUrl(), + ce: utils.cookiesAreEnabled(), + dpr: topWindow.devicePixelRatio || 1 } - - const _handleGumGumResponse = cachedBidRequest => (bidResponse = {}) => { - const { pi: productId - } = cachedBidRequest; - const { ad = {} - , pag = {} - , thms: throttle - } = bidResponse; - /* cache the pageViewId */ - if (pag && pag.pvid) pageViewId = pag.pvid; - if (ad && ad.id) { - /* set the new throttle */ - throttleTable[productId] = throttle || defaultThrottle; - /* create the bid */ - const bid = bidfactory.createBid(1); - const { t: trackingId - } = pag; - bidResponse.request = cachedBidRequest; - const encodedResponse = encodeURIComponent(JSON.stringify(bidResponse)); - const gumgumAdLoader = ``; - Object.assign(bid, { - cpm: ad.price, - ad: gumgumAdLoader, - width: ad.width, - height: ad.height, - bidderCode: BIDDER_CODE - }); - bidmanager.addBidResponse(cachedBidRequest.placementCode, bid); - } else { - const noBid = bidfactory.createBid(2); - noBid.bidderCode = BIDDER_CODE; - bidmanager.addBidResponse(cachedBidRequest.placementCode, noBid); - } - delete CALLBACKS[cachedBidRequest.id]; + return browserParams +} + +function getWrapperCode(wrapper, data) { + return wrapper.replace('AD_JSON', window.btoa(JSON.stringify(data))) +} + +// TODO: use getConfig() +function _getDigiTrustQueryParams() { + function getDigiTrustId () { + var digiTrustUser = (window.DigiTrust && window.DigiTrust.getUser) ? window.DigiTrust.getUser(DT_CREDENTIALS) : {}; + return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || ''; }; - window.$$PREBID_GLOBAL$$.handleGumGumCB = CALLBACKS; - + let digiTrustId = getDigiTrustId(); + // Verify there is an ID and this user has not opted out + if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) { + return {}; + } return { - callBids: _callBids + 'dt': digiTrustId.id }; -}; - -adaptermanager.registerBidAdapter(new GumgumAdapter(), 'gumgum'); +} + +/** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ +function isBidRequestValid (bid) { + const { + params, + adUnitCode + } = bid; + + switch (true) { + case !!(params.inScreen): break; + case !!(params.inSlot): break; + default: + utils.logWarn(`[GumGum] No product selected for the placement ${adUnitCode}, please check your implementation.`); + return false; + } + return true; +} + +/** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ +function buildRequests (validBidRequests) { + const bids = []; + utils._each(validBidRequests, bidRequest => { + const timeout = config.getConfig('bidderTimeout'); + const { + bidId, + params = {}, + transactionId + } = bidRequest; + const data = {} + + if (params.inScreen) { + data.t = params.inScreen; + data.pi = 2; + } + if (params.inSlot) { + data.si = parseInt(params.inSlot, 10); + data.pi = 3; + } -module.exports = GumgumAdapter; + bids.push({ + id: bidId, + tmax: timeout, + tId: transactionId, + pi: data.pi, + sizes: bidRequest.sizes, + url: BID_ENDPOINT, + method: 'GET', + data: Object.assign(data, _getBrowserParams(), _getDigiTrustQueryParams()) + }) + }); + return bids; +} + +/** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function interpretResponse (serverResponse, bidRequest) { + const bidResponses = [] + const serverResponseBody = serverResponse.body + const { + ad: { + price: cpm, + id: creativeId, + markup + }, + cw: wrapper + } = serverResponseBody + let isTestUnit = (bidRequest.data && bidRequest.data.pi === 3 && bidRequest.data.si === 9) + let [width, height] = utils.parseSizesInput(bidRequest.sizes)[0].split('x') + + if (creativeId) { + bidResponses.push({ + // dealId: DEAL_ID, + // referrer: REFERER, + ad: wrapper ? getWrapperCode(wrapper, Object.assign({}, serverResponseBody, { bidRequest })) : markup, + cpm: isTestUnit ? 0.1 : cpm, + creativeId, + currency: 'USD', + height, + netRevenue: true, + requestId: bidRequest.id, + ttl: TIME_TO_LIVE, + width + }) + } + return bidResponses +} + +export const spec = { + code: BIDDER_CODE, + aliases: ALIAS_BIDDER_CODE, + isBidRequestValid, + buildRequests, + interpretResponse +} +registerBidder(spec) diff --git a/modules/gumgumBidAdapter.md b/modules/gumgumBidAdapter.md new file mode 100644 index 00000000000..500c2a49e3b --- /dev/null +++ b/modules/gumgumBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +``` +Module Name: GumGum Bidder Adapter +Module Type: Bidder Adapter +Maintainer: engineering@gumgum.com +``` + +# Description + +GumGum adapter for Prebid.js 1.0 + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: 'gumgum', + params: { + inSlot: '9' // GumGum Slot ID given to the client + } + } + ] + },{ + code: 'test-div', + sizes: [[300, 50]], + bids: [ + { + bidder: 'gumgum', + params: { + inScreen: 'ggumtest' // GumGum Zone ID given to the client + } + } + ] + } +]; +``` diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index b90a1a48b15..30627d4d12d 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -1,295 +1,138 @@ -import {expect} from 'chai'; -import Adapter from '../../../modules/gumgumBidAdapter'; -import bidManager from '../../../src/bidmanager'; -import adLoader from '../../../src/adloader'; -import * as utils from '../../../src/utils'; -import { STATUS } from '../../../src/constants'; +import { expect } from 'chai'; +import { newBidder } from 'src/adapters/bidderFactory'; +import { spec } from 'modules/gumgumBidAdapter'; -describe('gumgum adapter', () => { - 'use strict'; +const ENDPOINT = 'https://g2.gumgum.com/hbid/imp'; - let adapter; - let sandbox; +describe('gumgumAdapter', () => { + const adapter = newBidder(spec); - const TEST = { - PUBLISHER_IDENTITY: 'ggumtest', - BIDDER_CODE: 'gumgum', - PLACEMENT: 'placementId', - CPM: 2 - }; - const bidderRequest = { - bidderCode: TEST.BIDDER_CODE, - bids: [{ // in-screen - bidId: 'InScreenBidId', - bidder: TEST.BIDDER_CODE, - placementCode: TEST.PLACEMENT, - sizes: [ [728, 90] ], - params: { - inScreen: TEST.PUBLISHER_IDENTITY - } - }, { // in-image - bidId: 'InImageBidId', - bidder: TEST.BIDDER_CODE, - placementCode: TEST.PLACEMENT, - sizes: [ [728, 90] ], - params: { - inImage: TEST.PUBLISHER_IDENTITY - } - }, { // native - bidId: 'NativeBidId', - bidder: TEST.BIDDER_CODE, - placementCode: TEST.PLACEMENT, - sizes: [ [728, 90] ], - params: { - native: 10 - } - }, { // slot - bidId: 'InSlotBidId', - bidder: TEST.BIDDER_CODE, - placementCode: TEST.PLACEMENT, - sizes: [ [728, 90] ], - params: { - inSlot: 10 - } - }, { // no identity - bidId: 'NoIdentityBidId', - bidder: TEST.BIDDER_CODE, - placementCode: TEST.PLACEMENT, - sizes: [ [728, 90] ] - }] - }; - const pageParams = { - 'pvid': 'PVID' - }; - const bidderResponse = { - 'ad': { - 'id': 1, - 'width': 728, - 'height': 90, - 'markup': '
some fancy ad
', - 'ii': true, - 'du': 'http://example.com/', - 'price': TEST.CPM, - 'impurl': 'http://example.com/' - }, - 'pag': pageParams - }; - - function mockBidResponse(response) { - sandbox.stub(bidManager, 'addBidResponse'); - sandbox.stub(adLoader, 'loadScript'); - adapter.callBids(bidderRequest); - $$PREBID_GLOBAL$$.handleGumGumCB['InScreenBidId'](response); - return bidManager.addBidResponse.firstCall.args[1]; - } - - beforeEach(() => { - adapter = new Adapter(); - sandbox = sinon.sandbox.create(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('DigiTrust params', () => { - beforeEach(() => { - sandbox.stub(adLoader, 'loadScript'); - }); - - it('should send digiTrust params', () => { - window.DigiTrust = { - getUser: function() {} - }; - sandbox.stub(window.DigiTrust, 'getUser', () => - ({ - success: true, - identity: { - privacy: {optout: false}, - id: 'testId' - } - }) - ); - - adapter.callBids(bidderRequest); - expect(adLoader.loadScript.firstCall.args[0]).to.include('&dt=testId'); - delete window.DigiTrust; - }); - - it('should not send DigiTrust params when DigiTrust is not loaded', () => { - adapter.callBids(bidderRequest); - expect(adLoader.loadScript.firstCall.args[0]).to.not.include('&dt'); + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); }); + }); - it('should not send DigiTrust params due to opt out', () => { - window.DigiTrust = { - getUser: function() {} + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'gumgum', + 'params': { + 'inScreen': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true when required params found', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'inSlot': '789' }; - sandbox.stub(window.DigiTrust, 'getUser', () => - ({ - success: true, - identity: { - privacy: {optout: true}, - id: 'testId' - } - }) - ); - adapter.callBids(bidderRequest); - expect(adLoader.loadScript.firstCall.args[0]).to.not.include('&dt'); - delete window.DigiTrust; + expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should not send DigiTrust params on failure', () => { - window.DigiTrust = { - getUser: function() {} + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placementId': 0 }; - sandbox.stub(window.DigiTrust, 'getUser', () => - ({ - success: false, - identity: { - privacy: {optout: false}, - id: 'testId' - } - }) - ); - - adapter.callBids(bidderRequest); - expect(adLoader.loadScript.firstCall.args[0]).to.not.include('&dt'); - delete window.DigiTrust; - }); - }); - - describe('callBids', () => { - beforeEach(() => { - sandbox.stub(adLoader, 'loadScript'); - adapter.callBids(bidderRequest); - }); - - it('calls the endpoint once per valid bid', () => { - sinon.assert.callCount(adLoader.loadScript, 4); - }); - - it('includes required browser data', () => { - const endpointRequest = expect(adLoader.loadScript.firstCall.args[0]); - endpointRequest.to.include('vw'); - endpointRequest.to.include('vh'); - endpointRequest.to.include('sw'); - endpointRequest.to.include('sh'); - }); - - it('includes the global bid timeout', () => { - const endpointRequest = expect(adLoader.loadScript.firstCall.args[0]); - endpointRequest.to.include(`tmax=${$$PREBID_GLOBAL$$.cbTimeout}`); - }); - - it('includes the publisher identity', () => { - const endpointRequest = expect(adLoader.loadScript.firstCall.args[0]); - endpointRequest.to.include('t=' + TEST.PUBLISHER_IDENTITY); - }); - - it('first call should be in-screen', () => { - expect(adLoader.loadScript.firstCall.args[0]).to.include('pi=2'); - }); - - it('second call should be in-image', () => { - expect(adLoader.loadScript.secondCall.args[0]).to.include('pi=1'); - }); - - it('third call should be native', () => { - expect(adLoader.loadScript.thirdCall.args[0]).to.include('pi=5'); - }); - - it('last call should be slot', () => { - expect(adLoader.loadScript.lastCall.args[0]).to.include('pi=3'); + expect(spec.isBidRequestValid(bid)).to.equal(false); }); }); - describe('handleGumGumCB[...]', () => { - it('exists and is function', () => { - expect($$PREBID_GLOBAL$$.handleGumGumCB['InScreenBidId']).to.exist.and.to.be.a('function'); - }); - }); - - describe('respond with a successful bid', () => { - let successfulBid; - - beforeEach(() => { - successfulBid = mockBidResponse(bidderResponse); - }); - - it('adds one bid', () => { - sinon.assert.calledOnce(bidManager.addBidResponse); - }); - - it('passes the correct placement code as the first param', () => { - const [ placementCode ] = bidManager.addBidResponse.firstCall.args; - expect(placementCode).to.eql(TEST.PLACEMENT); - }); - - it('has a GOOD status code', () => { - const STATUS_CODE = successfulBid.getStatusCode(); - expect(STATUS_CODE).to.eql(STATUS.GOOD); - }); - - it('uses the CPM returned by the server', () => { - expect(successfulBid).to.have.property('cpm', TEST.CPM); - }); - - it('has an ad', () => { - expect(successfulBid).to.have.property('ad'); - }); - - it('has the size specified by the server', () => { - expect(successfulBid).to.have.property('width', 728); - expect(successfulBid).to.have.property('height', 90); - }); - }); - - describe('respond with an empty bid', () => { - let noBid; - - beforeEach(() => { - noBid = mockBidResponse(undefined); - }); - - it('adds one bid', () => { - sinon.assert.calledOnce(bidManager.addBidResponse); - }); - - it('has a NO_BID status code', () => { - expect(noBid.getStatusCode()).to.eql(STATUS.NO_BID); - }); - - it('passes the correct placement code as the first parameter', () => { - const [ placementCode ] = bidManager.addBidResponse.firstCall.args; - expect(placementCode).to.eql(TEST.PLACEMENT); - }); - - it('adds the bidder code to the bid object', () => { - expect(noBid).to.have.property('bidderCode', TEST.BIDDER_CODE); - }); - }); - - describe('refresh throttle', () => { - beforeEach(() => { - mockBidResponse(bidderResponse); - }); - - afterEach(() => { - if (utils.logWarn.restore) { - utils.logWarn.restore(); + describe('buildRequests', () => { + let bidRequests = [ + { + 'bidder': 'gumgum', + 'params': { + 'inSlot': '9' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e' } + ]; + + it('sends bid request to ENDPOINT via GET', () => { + const requests = spec.buildRequests(bidRequests); + const request = requests[0]; + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('GET'); + expect(request.id).to.equal('30b31c1838de1e'); + }); + }) + + describe('interpretResponse', () => { + let serverResponse = { + 'ad': { + 'id': 29593, + 'width': 300, + 'height': 250, + 'ipd': 2000, + 'markup': '

I am an ad

', + 'ii': true, + 'du': null, + 'price': 0, + 'zi': 0, + 'impurl': 'http://g2.gumgum.com/ad/view', + 'clsurl': 'http://g2.gumgum.com/ad/close' + }, + 'pag': { + 't': 'ggumtest', + 'pvid': 'aa8bbb65-427f-4689-8cee-e3eed0b89eec', + 'css': 'html { overflow-y: auto }', + 'js': 'console.log("environment", env);' + }, + 'thms': 10000 + } + let bidRequest = { + id: 12345, + sizes: [[300, 250]], + url: ENDPOINT, + method: 'GET', + pi: 3 + } + + it('should get correct bid response', () => { + let expectedResponse = { + 'ad': '

I am an ad

', + 'cpm': 0, + 'creativeId': 29593, + 'currency': 'USD', + 'height': '250', + 'netRevenue': true, + 'requestId': 12345, + 'width': '300', + // dealId: DEAL_ID, + // referrer: REFERER, + ttl: 60 + }; + expect(spec.interpretResponse({ body: serverResponse }, bidRequest)).to.deep.equal([expectedResponse]); + }); + + it('handles nobid responses', () => { + let response = { + 'ad': {}, + 'pag': { + 't': 'ggumtest', + 'pvid': 'aa8bbb65-427f-4689-8cee-e3eed0b89eec', + 'css': 'html { overflow-y: auto }', + 'js': 'console.log("environment", env);' + }, + 'thms': 10000 + } + let result = spec.interpretResponse({ body: response }, bidRequest); + expect(result.length).to.equal(0); }); - - it('warns about the throttle limit', function() { - sinon.spy(utils, 'logWarn'); - // call all the binds again - adapter.callBids(bidderRequest); - // the timeout for in-screen should stop one bid request - const warning = expect(utils.logWarn.args[0][0]); - warning.to.include(TEST.PLACEMENT); - warning.to.include('inScreen'); - }); - }); + }) });