diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index f199e9fad30..f10fd48502f 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -1,128 +1,165 @@ -const utils = require('src/utils.js'); -const bidfactory = require('src/bidfactory.js'); -const bidmanager = require('src/bidmanager.js'); -const ajax = require('src/ajax.js'); -const CONSTANTS = require('src/constants.json'); -const adaptermanager = require('src/adaptermanager'); -const QUANTCAST_CALLBACK_URL = 'http://global.qc.rtb.quantserve.com:8080/qchb'; - -var QuantcastAdapter = function QuantcastAdapter() { - const BIDDER_CODE = 'quantcast'; - - const DEFAULT_BID_FLOOR = 0.0000000001; - let bidRequests = {}; - - let returnEmptyBid = function(bidId) { - var bidRequested = utils.getBidRequest(bidId); - if (!utils.isEmpty(bidRequested)) { - let bid = bidfactory.createBid(CONSTANTS.STATUS.NO_BID, bidRequested); - bid.bidderCode = BIDDER_CODE; - bidmanager.addBidResponse(bidRequested.placementCode, bid); +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'quantcast'; +const DEFAULT_BID_FLOOR = 0.0000000001; + +export const QUANTCAST_CALLBACK_URL = 'global.qc.rtb.quantserve.com'; +export const QUANTCAST_CALLBACK_URL_TEST = 's2s-canary.quantserve.com'; +export const QUANTCAST_NET_REVENUE = true; +export const QUANTCAST_TEST_PUBLISHER = 'test-publisher'; +export const QUANTCAST_TTL = 4; + +/** + * The documentation for Prebid.js Adapter 1.0 can be found at link below, + * http://prebid.org/dev-docs/bidder-adapter-1.html + */ +export const spec = { + code: BIDDER_CODE, + + /** + * Verify the `AdUnits.bids` response with `true` for valid request and `false` + * for invalid request. + * + * @param {object} bid + * @return boolean `true` is this is a valid bid, and `false` otherwise + */ + isBidRequestValid(bid) { + if (!bid) { + return false; } - }; - // expose the callback to the global object: - $$PREBID_GLOBAL$$.handleQuantcastCB = function (responseText) { - if (utils.isEmpty(responseText)) { - return; - } - let response = null; - try { - response = JSON.parse(responseText); - } catch (e) { - // Malformed JSON - utils.logError("Malformed JSON received from server - can't do anything here"); - return; - } - - if (response === null || !response.hasOwnProperty('bids') || utils.isEmpty(response.bids)) { - utils.logError("Sub-optimal JSON received from server - can't do anything here"); - return; + if (bid.mediaType === 'video') { + return false; } - for (let i = 0; i < response.bids.length; i++) { - let seatbid = response.bids[i]; - let key = seatbid.placementCode; - var request = bidRequests[key]; - if (request === null || request === undefined) { - return returnEmptyBid(seatbid.placementCode); - } - // This line is required since this is the field - // that bidfactory.createBid looks for - request.bidId = request.imp[0].placementCode; - let responseBid = bidfactory.createBid(CONSTANTS.STATUS.GOOD, request); - - responseBid.cpm = seatbid.cpm; - responseBid.ad = seatbid.ad; - responseBid.height = seatbid.height; - responseBid.width = seatbid.width; - responseBid.bidderCode = response.bidderCode; - responseBid.requestId = request.requestId; - responseBid.bidderCode = BIDDER_CODE; - - bidmanager.addBidResponse(request.bidId, responseBid); + return true; + }, + + /** + * Make a server request when the page asks Prebid.js for bids from a list of + * `BidRequests`. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be send to Quantcast server + * @return ServerRequest information describing the request to the server. + */ + buildRequests(bidRequests) { + const bids = bidRequests || []; + + const referrer = utils.getTopWindowUrl(); + const loc = utils.getTopWindowLocation(); + const domain = loc.hostname; + + let publisherTagURL; + let publisherTagURLTest; + + // Switch the callback URL to Quantcast Canary Endpoint for testing purpose + // `//` is not used because we have different port setting at our end + switch (window.location.protocol) { + case 'https:': + publisherTagURL = `https://${QUANTCAST_CALLBACK_URL}:8443/qchb`; + publisherTagURLTest = `https://${QUANTCAST_CALLBACK_URL_TEST}:8443/qchb`; + break; + default: + publisherTagURL = `http://${QUANTCAST_CALLBACK_URL}:8080/qchb`; + publisherTagURLTest = `http://${QUANTCAST_CALLBACK_URL_TEST}:8080/qchb`; } - }; - function callBids(params) { - let bids = params.bids || []; - if (bids.length === 0) { - return; - } - - let referrer = utils.getTopWindowUrl(); - let loc = utils.getTopWindowLocation(); - let domain = loc.hostname; - let publisherId = 0; + const bidRequestsList = bids.map(bid => { + const bidSizes = []; - publisherId = '' + bids[0].params.publisherId; - utils._each(bids, function(bid) { - let key = bid.placementCode; - var bidSizes = []; - utils._each(bid.sizes, function (size) { + bid.sizes.forEach(size => { bidSizes.push({ - 'width': size[0], - 'height': size[1] + width: size[0], + height: size[1] }); }); - bidRequests[key] = bidRequests[key] || { - 'publisherId': publisherId, - 'requestId': bid.bidId, - 'bidId': bid.bidId, - 'site': { - 'page': loc.href, - 'referrer': referrer, - 'domain': domain, + // Request Data Format can be found at https://wiki.corp.qc/display/adinf/QCX + const requestData = { + publisherId: bid.params.publisherId, + requestId: bid.bidId, + imp: [ + { + banner: { + battr: bid.params.battr, + sizes: bidSizes + }, + placementCode: bid.placementCode, + bidFloor: bid.params.bidFloor || DEFAULT_BID_FLOOR + } + ], + site: { + page: loc.href, + referrer, + domain }, - 'imp': [{ - - 'banner': { - 'battr': bid.params.battr, - 'sizes': bidSizes, - }, - 'placementCode': bid.placementCode, - 'bidFloor': bid.params.bidFloor || DEFAULT_BID_FLOOR, - }] + bidId: bid.bidId }; - }); - utils._each(bidRequests, function (bidRequest) { - ajax.ajax(QUANTCAST_CALLBACK_URL, $$PREBID_GLOBAL$$.handleQuantcastCB, JSON.stringify(bidRequest), { + const data = JSON.stringify(requestData); + + const url = + bid.params.publisherId === QUANTCAST_TEST_PUBLISHER + ? publisherTagURLTest + : publisherTagURL; + + return { + data, method: 'POST', - withCredentials: true - }); + url + }; }); - } - // Export the `callBids` function, so that Prebid.js can execute - // this function when the page asks to send out bid requests. - return { - callBids: callBids, - QUANTCAST_CALLBACK_URL: QUANTCAST_CALLBACK_URL - }; -}; + return bidRequestsList; + }, + + /** + * Function get called when the browser has received the response from Quantcast server. + * The function parse the response and create a `bidResponse` object containing one/more bids. + * Returns an empty array if no valid bids + * + * Response Data Format can be found at https://wiki.corp.qc/display/adinf/QCX + * + * @param {*} serverResponse A successful response from Quantcast server. + * @return {Bid[]} An array of bids which were nested inside the server. + * + */ + interpretResponse(serverResponse) { + if (serverResponse === undefined) { + utils.logError('Server Response is undefined'); + return []; + } + + const response = serverResponse['body']; + + if ( + response === undefined || + !response.hasOwnProperty('bids') || + utils.isEmpty(response.bids) + ) { + utils.logError('Sub-optimal JSON received from Quantcast server'); + return []; + } -adaptermanager.registerBidAdapter(new QuantcastAdapter(), 'quantcast'); + const bidResponsesList = response.bids.map(bid => { + const { ad, cpm, width, height, creativeId, currency } = bid; + + return { + requestId: response.requestId, + cpm, + width, + height, + ad, + ttl: QUANTCAST_TTL, + creativeId, + netRevenue: QUANTCAST_NET_REVENUE, + currency + }; + }); + + return bidResponsesList; + } +}; -module.exports = QuantcastAdapter; +registerBidder(spec); diff --git a/modules/quantcastBidAdapter.md b/modules/quantcastBidAdapter.md new file mode 100644 index 00000000000..20cf25bffbf --- /dev/null +++ b/modules/quantcastBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +``` +Module Name: Quantcast Bidder Adapter +Module Type: Bidder Adapter +Maintainer: xli@quantcast.com +``` + +# Description + +Module that connects to Quantcast demand sources to fetch bids. + +# Test Parameters + +```js +const adUnits = [{ + code: 'banner', + sizes: [ + [300, 250] + ], + bids: [ + { + bidder: 'quantcast', + params: { + publisherId: 'test-publisher', // REQUIRED - Publisher ID provided by Quantcast + battr: [1, 2] // OPTIONAL - Array of blocked creative attributes as per OpenRTB Spec List 5.3 + } + } + ] +}]; +``` \ No newline at end of file diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index 05748d85845..14981f198b6 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -1,231 +1,216 @@ -import {expect} from 'chai'; -import Adapter from '../../../modules/quantcastBidAdapter'; -import * as ajax from 'src/ajax'; -import bidManager from '../../../src/bidmanager'; -import adLoader from '../../../src/adloader'; - -describe('quantcast adapter', () => { - let bidsRequestedOriginal; - let adapter; - let sandbox; - let ajaxStub; - - const bidderRequest = { - bidderCode: 'quantcast', - requestId: '595ffa73-d78a-46c9-b18e-f99548a5be6b', - bidderRequestId: '1cc026909c24c8', - bids: [ - { - bidId: '2f7b179d443f14', - bidder: 'quantcast', - placementCode: 'div-gpt-ad-1438287399331-0', - sizes: [[300, 250], [300, 600]], - params: { - publisherId: 'test-publisher', - battr: [1, 2], - } - } - ] - }; +import * as utils from 'src/utils'; +import { expect } from 'chai'; +import { + QUANTCAST_CALLBACK_URL_TEST, + QUANTCAST_CALLBACK_URL, + QUANTCAST_NET_REVENUE, + QUANTCAST_TTL, + spec as qcSpec +} from '../../../modules/quantcastBidAdapter'; +import { newBidder } from '../../../src/adapters/bidderFactory'; + +describe('Quantcast adapter', () => { + const quantcastAdapter = newBidder(qcSpec); + let bidRequest; beforeEach(() => { - bidsRequestedOriginal = $$PREBID_GLOBAL$$._bidsRequested; - $$PREBID_GLOBAL$$._bidsRequested = []; - - adapter = new Adapter(); - sandbox = sinon.sandbox.create(); - ajaxStub = sandbox.stub(ajax, 'ajax'); - }); - - afterEach(() => { - sandbox.restore(); - - $$PREBID_GLOBAL$$._bidsRequested = bidsRequestedOriginal; - }); - - describe('sizes', () => { - let bidderRequest = { - bidderCode: 'quantcast', + bidRequest = { + bidder: 'quantcast', + bidId: '2f7b179d443f14', requestId: '595ffa73-d78a-46c9-b18e-f99548a5be6b', bidderRequestId: '1cc026909c24c8', - bids: [ - { - bidId: '2f7b179d443f14', - bidder: 'quantcast', - placementCode: 'div-gpt-ad-1438287399331-0', - sizes: [[300, 250], [300, 600]], - params: { - publisherId: 'test-publisher', - battr: [1, 2], - } - } - ] + placementCode: 'div-gpt-ad-1438287399331-0', + params: { + publisherId: 'test-publisher', // REQUIRED - Publisher ID provided by Quantcast + battr: [1, 2] // OPTIONAL - Array of blocked creative attributes as per OpenRTB Spec List 5.3 + }, + sizes: [[300, 250]] }; + }); - it('should not call server when empty input is provided', () => { - adapter.callBids({}); - sinon.assert.notCalled(ajaxStub); + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(quantcastAdapter.callBids).to.exist.and.to.be.a('function'); }); + }); - it('should call server once even when multiple sizes are passed', () => { - adapter.callBids(bidderRequest); - sinon.assert.calledOnce(ajaxStub); - - expect(ajaxStub.firstCall.args[0]).to.eql(adapter.QUANTCAST_CALLBACK_URL); - expect(ajaxStub.firstCall.args[1]).to.exist.and.to.be.a('function'); - expect(ajaxStub.firstCall.args[2]).to.include('div-gpt-ad-1438287399331-0'); - expect(ajaxStub.firstCall.args[2]).to.include('test-publisher'); - expect(ajaxStub.firstCall.args[2]).to.include('2f7b179d443f14'); - expect(ajaxStub.firstCall.args[3]).to.eql({method: 'POST', withCredentials: true}); + describe('`isBidRequestValid`', () => { + it('should return `false` when bid is not passed', () => { + expect(qcSpec.isBidRequestValid()).to.equal(false); }); - it('should call server once when one size is passed', () => { - bidderRequest.bids[0].sizes = [728, 90]; - adapter.callBids(bidderRequest); - sinon.assert.calledOnce(ajaxStub); + it('should return `false` when bid `mediaType` is `video`', () => { + const bidRequest = { mediaType: 'video' }; - expect(ajaxStub.firstCall.args[0]).to.eql(adapter.QUANTCAST_CALLBACK_URL); - expect(ajaxStub.firstCall.args[1]).to.exist.and.to.be.a('function'); - expect(ajaxStub.firstCall.args[3]).to.eql({method: 'POST', withCredentials: true}); + expect(qcSpec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('should call server once when size is passed as string', () => { - bidderRequest.bids[0].sizes = '728x90'; - adapter.callBids(bidderRequest); - sinon.assert.calledOnce(ajaxStub); + it('should return `true` when bid contains required params', () => { + const bidRequest = { mediaType: 'banner' }; - expect(ajaxStub.firstCall.args[0]).to.eql(adapter.QUANTCAST_CALLBACK_URL); - expect(ajaxStub.firstCall.args[1]).to.exist.and.to.be.a('function'); - expect(ajaxStub.firstCall.args[3]).to.eql({method: 'POST', withCredentials: true}); + expect(qcSpec.isBidRequestValid(bidRequest)).to.equal(true); }); + }); - it('should call server once when sizes are passed as a comma-separated string', () => { - bidderRequest.bids[0].sizes = '728x90,360x240'; - adapter.callBids(bidderRequest); - sinon.assert.calledOnce(ajaxStub); + describe('`buildRequests`', () => { + it('sends bid requests to Quantcast Canary Endpoint if `publisherId` is `test-publisher`', () => { + const requests = qcSpec.buildRequests([bidRequest]); + + switch (window.location.protocol) { + case 'https:': + expect(requests[0]['url']).to.equal( + `https://${QUANTCAST_CALLBACK_URL_TEST}:8443/qchb` + ); + break; + default: + expect(requests[0]['url']).to.equal( + `http://${QUANTCAST_CALLBACK_URL_TEST}:8080/qchb` + ); + break; + } + }); - expect(ajaxStub.firstCall.args[0]).to.eql(adapter.QUANTCAST_CALLBACK_URL); - expect(ajaxStub.firstCall.args[1]).to.exist.and.to.be.a('function'); - expect(ajaxStub.firstCall.args[3]).to.eql({method: 'POST', withCredentials: true}); + it('sends bid requests to Quantcast Global Endpoint for regular `publisherId`', () => { + const bidRequest = { + bidder: 'quantcast', + bidId: '2f7b179d443f14', + requestId: '595ffa73-d78a-46c9-b18e-f99548a5be6b', + bidderRequestId: '1cc026909c24c8', + placementCode: 'div-gpt-ad-1438287399331-0', + params: { + publisherId: 'regular-publisher', // REQUIRED - Publisher ID provided by Quantcast + battr: [1, 2] // OPTIONAL - Array of blocked creative attributes as per OpenRTB Spec List 5.3 + }, + sizes: [[300, 250]] + }; + const requests = qcSpec.buildRequests([bidRequest]); + + switch (window.location.protocol) { + case 'https:': + expect(requests[0]['url']).to.equal( + `https://${QUANTCAST_CALLBACK_URL}:8443/qchb` + ); + break; + default: + expect(requests[0]['url']).to.equal( + `http://${QUANTCAST_CALLBACK_URL}:8080/qchb` + ); + break; + } }); - }); - describe('multiple requests', () => { - let bidderRequest = { - bidderCode: 'quantcast', - requestId: '595ffa73-d78a-46c9-b18e-f99548a5be6b', - bidderRequestId: '1cc026909c24c8', - bids: [ - { - bidId: '2f7b179d443f14', - bidder: 'quantcast', - placementCode: 'div-gpt-ad-1438287399331-0', - sizes: [[300, 250]], - params: { - publisherId: 'test-publisher', - battr: [1, 2], - } - }, { - bidId: '2f7b179d443f15', - bidder: 'quantcast', - placementCode: 'div-gpt-ad-1438287399331-1', - sizes: [[300, 600]], - params: { - publisherId: 'test-publisher', - battr: [1, 2], - } - } - ] - }; + it('sends bid requests to Quantcast Header Bidding Endpoints via POST', () => { + const requests = qcSpec.buildRequests([bidRequest]); - it('request is fired twice for two bids', () => { - adapter.callBids(bidderRequest); - sinon.assert.calledTwice(ajaxStub); + expect(requests[0].method).to.equal('POST'); + }); - let firstReq = JSON.parse(ajaxStub.firstCall.args[2]); - expect(firstReq.requestId).to.eql('2f7b179d443f14'); - expect(firstReq.imp[0].placementCode).to.eql('div-gpt-ad-1438287399331-0'); + it('sends bid requests contains all the required parameters', () => { + const referrer = utils.getTopWindowUrl(); + const loc = utils.getTopWindowLocation(); + const domain = loc.hostname; - let secondReq = JSON.parse(ajaxStub.secondCall.args[2]); - expect(secondReq.requestId).to.eql('2f7b179d443f15'); - expect(secondReq.imp[0].placementCode).to.eql('div-gpt-ad-1438287399331-1'); + const requests = qcSpec.buildRequests([bidRequest]); + const expectedBidRequest = { + publisherId: 'test-publisher', + requestId: '2f7b179d443f14', + imp: [ + { + banner: { + battr: [1, 2], + sizes: [{ width: 300, height: 250 }] + }, + placementCode: 'div-gpt-ad-1438287399331-0', + bidFloor: 1e-10 + } + ], + site: { + page: loc.href, + referrer, + domain + }, + bidId: '2f7b179d443f14' + }; + + expect(requests[0].data).to.equal(JSON.stringify(expectedBidRequest)); }); }); - describe('handleQuantcastCB add bids to the manager', () => { - let firstBid; - let addBidReponseStub; - let bidsRequestedOriginal; - // respond - let bidderReponse = { - 'bidderCode': 'quantcast', - 'requestId': bidderRequest.requestId, - 'bids': [ + describe('`interpretResponse`', () => { + // The sample response is from https://wiki.corp.qc/display/adinf/QCX + const body = { + bidderCode: 'qcx', // Renaming it to use CamelCase since that is what is used in the Prebid.js variable name + requestId: 'erlangcluster@qa-rtb002.us-ec.adtech.com-11417780270886458', // Added this field. This is not used now but could be useful in troubleshooting later on. Specially for sites using iFrames + bids: [ { - 'statusCode': 1, - 'placementCode': bidderRequest.bids[0].bidId, - 'cpm': 4.5, - 'ad': '\n\n\n
\n
\n\n \n\nQuantcast\n\n
\n
', - 'width': 300, - 'height': 250 + statusCode: 1, + placementCode: 'imp1', // Changing this to placementCode to be reflective + cpm: 4.5, + currency: 'USD', + ad: + '
Quantcast
', + creativeId: 1001, + width: 300, + height: 250 } ] }; - beforeEach(() => { - bidsRequestedOriginal = $$PREBID_GLOBAL$$._bidsRequested; - addBidReponseStub = sandbox.stub(bidManager, 'addBidResponse'); - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - }); + const response = { + body, + headers: {} + }; - afterEach(() => { - sandbox.restore(); - $$PREBID_GLOBAL$$._bidsRequested = bidsRequestedOriginal; - }); + it('should return an empty array if `serverResponse` is `undefined`', () => { + const interpretedResponse = qcSpec.interpretResponse(); - it('should exist and be a function', () => { - expect($$PREBID_GLOBAL$$.handleQuantcastCB).to.exist.and.to.be.a('function'); + expect(interpretedResponse.length).to.equal(0); }); - it('should not add bid when empty text response comes', () => { - $$PREBID_GLOBAL$$.handleQuantcastCB(); - sinon.assert.notCalled(addBidReponseStub); - }); + it('should return an empty array if the parsed response does NOT include `bids`', () => { + const interpretedResponse = qcSpec.interpretResponse({}); - it('should not add bid when empty json response comes', () => { - $$PREBID_GLOBAL$$.handleQuantcastCB(JSON.stringify({})); - sinon.assert.notCalled(addBidReponseStub); + expect(interpretedResponse.length).to.equal(0); }); - it('should not add bid when malformed json response comes', () => { - $$PREBID_GLOBAL$$.handleQuantcastCB('non json text'); - sinon.assert.notCalled(addBidReponseStub); + it('should return an empty array if the parsed response has an empty `bids`', () => { + const interpretedResponse = qcSpec.interpretResponse({ bids: [] }); + + expect(interpretedResponse.length).to.equal(0); }); - it('should add a bid object for each bid', () => { - // You need the following call so that the in-memory storage of the bidRequest is carried out. Without this the callback won't work correctly. - adapter.callBids(bidderRequest); - $$PREBID_GLOBAL$$.handleQuantcastCB(JSON.stringify(bidderReponse)); - sinon.assert.calledOnce(addBidReponseStub); - expect(addBidReponseStub.firstCall.args[0]).to.eql('div-gpt-ad-1438287399331-0'); + it('should get correct bid response', () => { + const expectedResponse = { + requestId: 'erlangcluster@qa-rtb002.us-ec.adtech.com-11417780270886458', + cpm: 4.5, + width: 300, + height: 250, + ad: + '
Quantcast
', + ttl: QUANTCAST_TTL, + creativeId: 1001, + netRevenue: QUANTCAST_NET_REVENUE, + currency: 'USD' + }; + const interpretedResponse = qcSpec.interpretResponse(response); + + expect(interpretedResponse[0]).to.deep.equal(expectedResponse); }); - it('should return no bid even when requestId and sizes are missing', () => { - let bidderReponse = { - 'bidderCode': 'quantcast', - 'bids': [ - { - 'statusCode': 0, - 'placementCode': bidderRequest.bids[0].bidId, - } - ] + it('handles no bid response', () => { + const body = { + bidderCode: 'qcx', // Renaming it to use CamelCase since that is what is used in the Prebid.js variable name + requestId: 'erlangcluster@qa-rtb002.us-ec.adtech.com-11417780270886458', // Added this field. This is not used now but could be useful in troubleshooting later on. Specially for sites using iFrames + bids: [] + }; + const response = { + body, + headers: {} }; + const expectedResponse = []; + const interpretedResponse = qcSpec.interpretResponse(response); - // You need the following call so that the in-memory storage of the bidRequest is carried out. Without this the callback won't work correctly. - adapter.callBids(bidderRequest); - $$PREBID_GLOBAL$$.handleQuantcastCB(JSON.stringify(bidderReponse)); - // sinon.assert.calledOnce(addBidReponseStub); - // expect(addBidReponseStub.firstCall.args[0]).to.eql("div-gpt-ad-1438287399331-0"); + expect(interpretedResponse.length).to.equal(0); }); }); });