UNPKG

mk9-prebid

Version:

Header Bidding Management Library

1,591 lines (1,445 loc) 69.7 kB
import rubiconAnalyticsAdapter, { SEND_TIMEOUT, parseBidResponse, getHostNameFromReferer, storage, rubiConf, } from 'modules/rubiconAnalyticsAdapter.js'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr.js'; import * as mockGpt from '../integration/faker/googletag.js'; import { setConfig, addBidResponseHook, } from 'modules/currency.js'; let Ajv = require('ajv'); let schema = require('./rubiconAnalyticsSchema.json'); let ajv = new Ajv({ allErrors: true }); let validator = ajv.compile(schema); function validate(message) { validator(message); expect(validator.errors).to.deep.equal(null); } // using es6 "import * as events from 'src/events.js'" causes the events.getEvents stub not to work... let events = require('src/events.js'); let utils = require('src/utils.js'); const { EVENTS: { AUCTION_INIT, AUCTION_END, BID_REQUESTED, BID_RESPONSE, BIDDER_DONE, BID_WON, BID_TIMEOUT, SET_TARGETING } } = CONSTANTS; const BID = { 'bidder': 'rubicon', 'width': 640, 'height': 480, 'mediaType': 'video', 'statusMessage': 'Bid available', 'bidId': '2ecff0db240757', 'adId': 'fake_ad_id', 'source': 'client', 'requestId': '2ecff0db240757', 'currency': 'USD', 'creativeId': '3571560', 'cpm': 1.22752, 'ttl': 300, 'netRevenue': false, 'ad': '<html></html>', 'rubiconTargeting': { 'rpfl_elemid': '/19968336/header-bid-tag-0', 'rpfl_14062': '2_tier0100' }, 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'responseTimestamp': 1519149629415, 'requestTimestamp': 1519149628471, 'adUnitCode': '/19968336/header-bid-tag-0', 'timeToRespond': 944, 'pbLg': '1.00', 'pbMg': '1.20', 'pbHg': '1.22', 'pbAg': '1.20', 'pbDg': '1.22', 'pbCg': '', 'size': '640x480', 'adserverTargeting': { 'hb_bidder': 'rubicon', 'hb_adid': '2ecff0db240757', 'hb_pb': 1.20, 'hb_size': '640x480', 'hb_source': 'client' }, getStatusCode() { return 1; } }; const BID2 = Object.assign({}, BID, { adUnitCode: '/19968336/header-bid-tag1', bidId: '3bd4ebb1c900e2', adId: 'fake_ad_id', requestId: '3bd4ebb1c900e2', width: 728, height: 90, mediaType: 'banner', cpm: 1.52, source: 'server', seatBidId: 'aaaa-bbbb-cccc-dddd', rubiconTargeting: { 'rpfl_elemid': '/19968336/header-bid-tag1', 'rpfl_14062': '2_tier0100' }, adserverTargeting: { 'hb_bidder': 'rubicon', 'hb_adid': '3bd4ebb1c900e2', 'hb_pb': '1.500', 'hb_size': '728x90', 'hb_source': 'server' } }); const BID3 = Object.assign({}, BID, { adUnitCode: '/19968336/siderail-tag1', bidId: '5fg6hyy4r879f0', adId: 'fake_ad_id', requestId: '5fg6hyy4r879f0', width: 300, height: 250, mediaType: 'banner', cpm: 2.01, source: 'server', seatBidId: 'aaaa-bbbb-cccc-dddd', rubiconTargeting: { 'rpfl_elemid': '/19968336/siderail-tag1', 'rpfl_14062': '15_tier0200' }, adserverTargeting: { 'hb_bidder': 'rubicon', 'hb_adid': '5fg6hyy4r879f0', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'server' } }); const BID4 = Object.assign({}, BID, { adUnitCode: '/19968336/header-bid-tag1', bidId: '3bd4ebb1c900e2', adId: 'fake_ad_id', requestId: '3bd4ebb1c900e2', width: 728, height: 90, mediaType: 'banner', cpm: 1.52, source: 'server', pbsBidId: 'zzzz-yyyy-xxxx-wwww', seatBidId: 'aaaa-bbbb-cccc-dddd', rubiconTargeting: { 'rpfl_elemid': '/19968336/header-bid-tag1', 'rpfl_14062': '2_tier0100' }, adserverTargeting: { 'hb_bidder': 'rubicon', 'hb_adid': '3bd4ebb1c900e2', 'hb_pb': '1.500', 'hb_size': '728x90', 'hb_source': 'server' } }); const floorMinRequest = { 'bidder': 'rubicon', 'params': { 'accountId': '14062', 'siteId': '70608', 'zoneId': '335918', 'userId': '12346', 'keywords': ['a', 'b', 'c'], 'inventory': {'rating': '4-star', 'prodtype': 'tech'}, 'visitor': {'ucat': 'new', 'lastsearch': 'iphone'}, 'position': 'atf' }, 'mediaTypes': { 'banner': { 'sizes': [[300, 250]] } }, 'adUnitCode': '/19968336/siderail-tag1', 'transactionId': 'c435626g-9e3f-401a-bee1-d56aec29a1d4', 'sizes': [[300, 250]], 'bidId': '5fg6hyy4r879f0', 'bidderRequestId': '1be65d7958826a', 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' }; const MOCK = { SET_TARGETING: { [BID.adUnitCode]: BID.adserverTargeting, [BID2.adUnitCode]: BID2.adserverTargeting, [BID3.adUnitCode]: BID3.adserverTargeting }, AUCTION_INIT: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'timestamp': 1519767010567, 'auctionStatus': 'inProgress', 'adUnits': [ { 'code': '/19968336/header-bid-tag1', 'sizes': [[640, 480]], 'bids': [ { 'bidder': 'rubicon', 'params': { 'accountId': 1001, 'siteId': 113932, 'zoneId': 535512 } } ], 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' } ], 'adUnitCodes': ['/19968336/header-bid-tag1'], 'bidderRequests': [ { 'bidderCode': 'rubicon', 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'bidderRequestId': '1be65d7958826a', 'bids': [ { 'bidder': 'rubicon', 'params': { 'accountId': 1001, 'siteId': 113932, 'zoneId': 535512 }, 'mediaTypes': { 'banner': { 'sizes': [[640, 480]] } }, 'adUnitCode': '/19968336/header-bid-tag1', 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', 'sizes': [[640, 480]], 'bidId': '2ecff0db240757', 'bidderRequestId': '1be65d7958826a', 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'src': 'client', 'bidRequestsCount': 1 } ], 'timeout': 3000, 'refererInfo': { 'referer': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] } } ], 'bidsReceived': [], 'winningBids': [], 'timeout': 3000, 'config': { 'accountId': 1001, 'endpoint': '//localhost:9999/event' } }, BID_REQUESTED: { 'bidder': 'rubicon', 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'bidderRequestId': '1be65d7958826a', 'bids': [ { 'bidder': 'rubicon', 'params': { 'accountId': '1001', 'siteId': '70608', 'zoneId': '335918', 'userId': '12346', 'keywords': ['a', 'b', 'c'], 'inventory': 'test', 'visitor': {'ucat': 'new', 'lastsearch': 'iphone'}, 'position': 'btf', 'video': { 'language': 'en', 'playerHeight': 480, 'playerWidth': 640, 'size_id': 203, 'skip': 1, 'skipdelay': 15, 'aeParams': { 'p_aso.video.ext.skip': '1', 'p_aso.video.ext.skipdelay': '15' } } }, 'mediaTypes': { 'video': { 'context': 'instream', 'playerSize': [640, 480] } }, 'adUnitCode': '/19968336/header-bid-tag-0', 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', 'sizes': [[640, 480]], 'bidId': '2ecff0db240757', 'bidderRequestId': '1be65d7958826a', 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'src': 'client' }, { 'bidder': 'rubicon', 'params': { 'accountId': '14062', 'siteId': '70608', 'zoneId': '335918', 'userId': '12346', 'keywords': ['a', 'b', 'c'], 'inventory': {'rating': '4-star', 'prodtype': 'tech'}, 'visitor': {'ucat': 'new', 'lastsearch': 'iphone'}, 'position': 'atf' }, 'mediaTypes': { 'banner': { 'sizes': [[1000, 300], [970, 250], [728, 90]] } }, 'adUnitCode': '/19968336/header-bid-tag1', 'transactionId': 'c116413c-9e3f-401a-bee1-d56aec29a1d4', 'sizes': [[1000, 300], [970, 250], [728, 90]], 'bidId': '3bd4ebb1c900e2', 'bidderRequestId': '1be65d7958826a', 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'src': 's2s' } ], 'auctionStart': 1519149536560, 'timeout': 5000, 'start': 1519149562216, 'refererInfo': { 'referer': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] } }, BID_RESPONSE: [ BID, BID2, BID3 ], AUCTION_END: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' }, BID_WON: [ Object.assign({}, BID, { 'status': 'rendered' }), Object.assign({}, BID2, { 'status': 'rendered' }), Object.assign({}, BID3, { 'status': 'rendered' }) ], BIDDER_DONE: { 'bidderCode': 'rubicon', 'serverResponseTimeMs': 42, 'bids': [ BID, Object.assign({}, BID2, { 'serverResponseTimeMs': 42, }), Object.assign({}, BID3, { 'serverResponseTimeMs': 55, }) ] }, BID_TIMEOUT: [ { 'bidId': '2ecff0db240757', 'bidder': 'rubicon', 'adUnitCode': '/19968336/header-bid-tag-0', 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' } ] }; const STUBBED_UUID = '12345678-1234-1234-1234-123456789abc'; const ANALYTICS_MESSAGE = { 'channel': 'web', 'integration': 'pbjs', 'version': '$prebid.version$', 'referrerUri': 'http://www.test.com/page.html', 'session': { 'expires': 1519788613781, 'id': STUBBED_UUID, 'start': 1519767013781 }, 'timestamps': { 'auctionEnded': 1519767013781, 'eventTime': 1519767013781, 'prebidLoaded': rubiconAnalyticsAdapter.MODULE_INITIALIZED_TIME }, 'trigger': 'allBidWons', 'referrerHostname': 'www.test.com', 'auctions': [ { 'requestId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'clientTimeoutMillis': 3000, 'serverTimeoutMillis': 1000, 'accountId': 1001, 'samplingFactor': 1, 'adUnits': [ { 'adUnitCode': '/19968336/header-bid-tag-0', 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', 'videoAdFormat': 'outstream', 'mediaTypes': [ 'video' ], 'dimensions': [ { 'width': 640, 'height': 480 } ], 'status': 'success', 'accountId': 1001, 'siteId': 70608, 'zoneId': 335918, 'adserverTargeting': { 'hb_bidder': 'rubicon', 'hb_adid': '2ecff0db240757', 'hb_pb': '1.200', 'hb_size': '640x480', 'hb_source': 'client' }, 'bids': [ { 'bidder': 'rubicon', 'bidId': '2ecff0db240757', 'status': 'success', 'source': 'client', 'clientLatencyMillis': 3214, 'params': { 'accountId': '1001', 'siteId': '70608', 'zoneId': '335918' }, 'bidResponse': { 'bidPriceUSD': 1.22752, 'dimensions': { 'width': 640, 'height': 480 }, 'mediaType': 'video' } } ] }, { 'adUnitCode': '/19968336/header-bid-tag1', 'transactionId': 'c116413c-9e3f-401a-bee1-d56aec29a1d4', 'mediaTypes': [ 'banner' ], 'dimensions': [ { 'width': 1000, 'height': 300 }, { 'width': 970, 'height': 250 }, { 'width': 728, 'height': 90 } ], 'status': 'success', 'adserverTargeting': { 'hb_bidder': 'rubicon', 'hb_adid': '3bd4ebb1c900e2', 'hb_pb': '1.500', 'hb_size': '728x90', 'hb_source': 'server' }, 'bids': [ { 'bidder': 'rubicon', 'bidId': 'aaaa-bbbb-cccc-dddd', 'status': 'success', 'source': 'server', 'clientLatencyMillis': 3214, 'serverLatencyMillis': 42, 'params': { 'accountId': '14062', 'siteId': '70608', 'zoneId': '335918' }, 'bidResponse': { 'bidPriceUSD': 1.52, 'dimensions': { 'width': 728, 'height': 90 }, 'mediaType': 'banner' } } ] } ] } ], 'bidsWon': [ { 'bidder': 'rubicon', 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', 'adUnitCode': '/19968336/header-bid-tag-0', 'bidId': '2ecff0db240757', 'status': 'success', 'source': 'client', 'clientLatencyMillis': 3214, 'samplingFactor': 1, 'accountId': 1001, 'siteId': 70608, 'zoneId': 335918, 'params': { 'accountId': '1001', 'siteId': '70608', 'zoneId': '335918' }, 'videoAdFormat': 'outstream', 'mediaTypes': [ 'video' ], 'adserverTargeting': { 'hb_bidder': 'rubicon', 'hb_adid': '2ecff0db240757', 'hb_pb': '1.200', 'hb_size': '640x480', 'hb_source': 'client' }, 'bidResponse': { 'bidPriceUSD': 1.22752, 'dimensions': { 'width': 640, 'height': 480 }, 'mediaType': 'video' }, 'bidwonStatus': 'success' }, { 'bidder': 'rubicon', 'transactionId': 'c116413c-9e3f-401a-bee1-d56aec29a1d4', 'adUnitCode': '/19968336/header-bid-tag1', 'bidId': 'aaaa-bbbb-cccc-dddd', 'status': 'success', 'source': 'server', 'clientLatencyMillis': 3214, 'serverLatencyMillis': 42, 'samplingFactor': 1, 'accountId': 1001, 'params': { 'accountId': '14062', 'siteId': '70608', 'zoneId': '335918' }, 'mediaTypes': [ 'banner' ], 'adserverTargeting': { 'hb_bidder': 'rubicon', 'hb_adid': '3bd4ebb1c900e2', 'hb_pb': '1.500', 'hb_size': '728x90', 'hb_source': 'server' }, 'bidResponse': { 'bidPriceUSD': 1.52, 'dimensions': { 'width': 728, 'height': 90 }, 'mediaType': 'banner' }, 'bidwonStatus': 'success' } ], 'wrapper': { 'name': '10000_fakewrapper_test' } }; function performStandardAuction(gptEvents) { events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); events.emit(AUCTION_END, MOCK.AUCTION_END); if (gptEvents && gptEvents.length) { gptEvents.forEach(gptEvent => mockGpt.emitEvent(gptEvent.eventName, gptEvent.params)); } events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[0]); events.emit(BID_WON, MOCK.BID_WON[1]); } describe('rubicon analytics adapter', function () { let sandbox; let clock; let getDataFromLocalStorageStub, setDataInLocalStorageStub, localStorageIsEnabledStub; beforeEach(function () { getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); setDataInLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); mockGpt.disable(); sandbox = sinon.sandbox.create(); localStorageIsEnabledStub.returns(true); sandbox.stub(events, 'getEvents').returns([]); sandbox.stub(utils, 'generateUUID').returns(STUBBED_UUID); clock = sandbox.useFakeTimers(1519767013781); rubiconAnalyticsAdapter.referrerHostname = ''; config.setConfig({ s2sConfig: { timeout: 1000, accountId: 10000, }, rubicon: { wrapperName: '10000_fakewrapper_test' } }) }); afterEach(function () { sandbox.restore(); config.resetConfig(); mockGpt.enable(); getDataFromLocalStorageStub.restore(); setDataInLocalStorageStub.restore(); localStorageIsEnabledStub.restore(); }); it('should require accountId', function () { sandbox.stub(utils, 'logError'); rubiconAnalyticsAdapter.enableAnalytics({ options: { endpoint: '//localhost:9999/event' } }); expect(utils.logError.called).to.equal(true); }); it('should require endpoint', function () { sandbox.stub(utils, 'logError'); rubiconAnalyticsAdapter.enableAnalytics({ options: { accountId: 1001 } }); expect(utils.logError.called).to.equal(true); }); describe('config subscribe', function() { it('should update the pvid if user asks', function () { expect(utils.generateUUID.called).to.equal(false); config.setConfig({rubicon: {updatePageView: true}}); expect(utils.generateUUID.called).to.equal(true); }); it('should merge in and preserve older set configs', function () { config.setConfig({ rubicon: { wrapperName: '1001_general', int_type: 'dmpbjs', fpkvs: { source: 'fb' } } }); expect(rubiConf).to.deep.equal({ analyticsEventDelay: 0, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', fpkvs: { source: 'fb' }, updatePageView: true }); // update it with stuff config.setConfig({ rubicon: { fpkvs: { link: 'email' } } }); expect(rubiConf).to.deep.equal({ analyticsEventDelay: 0, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', fpkvs: { source: 'fb', link: 'email' }, updatePageView: true }); // overwriting specific edge keys should update them config.setConfig({ rubicon: { fpkvs: { link: 'iMessage', source: 'twitter' } } }); expect(rubiConf).to.deep.equal({ analyticsEventDelay: 0, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', fpkvs: { link: 'iMessage', source: 'twitter' }, updatePageView: true }); }); }); describe('sampling', function () { beforeEach(function () { sandbox.stub(Math, 'random').returns(0.08); sandbox.stub(utils, 'logError'); }); afterEach(function () { rubiconAnalyticsAdapter.disableAnalytics(); }); describe('with options.samplingFactor', function () { it('should sample', function () { rubiconAnalyticsAdapter.enableAnalytics({ options: { endpoint: '//localhost:9999/event', accountId: 1001, samplingFactor: 10 } }); performStandardAuction(); expect(server.requests.length).to.equal(1); }); it('should unsample', function () { rubiconAnalyticsAdapter.enableAnalytics({ options: { endpoint: '//localhost:9999/event', accountId: 1001, samplingFactor: 20 } }); performStandardAuction(); expect(server.requests.length).to.equal(0); }); it('should throw errors for invalid samplingFactor', function () { rubiconAnalyticsAdapter.enableAnalytics({ options: { endpoint: '//localhost:9999/event', accountId: 1001, samplingFactor: 30 } }); performStandardAuction(); expect(server.requests.length).to.equal(0); expect(utils.logError.called).to.equal(true); }); }); describe('with options.sampling', function () { it('should sample', function () { rubiconAnalyticsAdapter.enableAnalytics({ options: { endpoint: '//localhost:9999/event', accountId: 1001, sampling: 0.1 } }); performStandardAuction(); expect(server.requests.length).to.equal(1); }); it('should unsample', function () { rubiconAnalyticsAdapter.enableAnalytics({ options: { endpoint: '//localhost:9999/event', accountId: 1001, sampling: 0.05 } }); performStandardAuction(); expect(server.requests.length).to.equal(0); }); it('should throw errors for invalid samplingFactor', function () { rubiconAnalyticsAdapter.enableAnalytics({ options: { endpoint: '//localhost:9999/event', accountId: 1001, sampling: 1 / 30 } }); performStandardAuction(); expect(server.requests.length).to.equal(0); expect(utils.logError.called).to.equal(true); }); }); }); describe('when handling events', function () { beforeEach(function () { rubiconAnalyticsAdapter.enableAnalytics({ options: { endpoint: '//localhost:9999/event', accountId: 1001 } }); }); afterEach(function () { rubiconAnalyticsAdapter.disableAnalytics(); }); it('should build a batched message from prebid events', function () { performStandardAuction(); expect(server.requests.length).to.equal(1); let request = server.requests[0]; expect(request.url).to.equal('//localhost:9999/event'); let message = JSON.parse(request.requestBody); validate(message); expect(message).to.deep.equal(ANALYTICS_MESSAGE); }); it('should pass along user ids', function () { let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); auctionInit.bidderRequests[0].bids[0].userId = { criteoId: 'sadfe4334', lotamePanoramaId: 'asdf3gf4eg', pubcid: 'dsfa4545-svgdfs5', sharedId: {id1: 'asdf', id2: 'sadf4344'} }; events.emit(AUCTION_INIT, auctionInit); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); events.emit(AUCTION_END, MOCK.AUCTION_END); events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[0]); events.emit(BID_WON, MOCK.BID_WON[1]); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); expect(message.auctions[0].user).to.deep.equal({ ids: [ {provider: 'criteoId', 'hasId': true}, {provider: 'lotamePanoramaId', 'hasId': true}, {provider: 'pubcid', 'hasId': true}, {provider: 'sharedId', 'hasId': true}, ] }); }); it('should handle bidResponse dimensions correctly', function () { events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); // mock bid response with playerWidth and playerHeight (NO width and height) let bidResponse1 = utils.deepClone(MOCK.BID_RESPONSE[0]); delete bidResponse1.width; delete bidResponse1.height; bidResponse1.playerWidth = 640; bidResponse1.playerHeight = 480; // mock bid response with no width height or playerwidth playerheight let bidResponse2 = utils.deepClone(MOCK.BID_RESPONSE[1]); delete bidResponse2.width; delete bidResponse2.height; delete bidResponse2.playerWidth; delete bidResponse2.playerHeight; events.emit(BID_RESPONSE, bidResponse1); events.emit(BID_RESPONSE, bidResponse2); events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); events.emit(AUCTION_END, MOCK.AUCTION_END); events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[0]); events.emit(BID_WON, MOCK.BID_WON[1]); let message = JSON.parse(server.requests[0].requestBody); validate(message); expect(message.auctions[0].adUnits[0].bids[0].bidResponse.dimensions).to.deep.equal({ width: 640, height: 480 }); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.dimensions).to.equal(undefined); }); it('should pass along adomians correctly', function () { events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); // 1 adomains let bidResponse1 = utils.deepClone(MOCK.BID_RESPONSE[0]); bidResponse1.meta = { advertiserDomains: ['magnite.com'] } // two adomains let bidResponse2 = utils.deepClone(MOCK.BID_RESPONSE[1]); bidResponse2.meta = { advertiserDomains: ['prebid.org', 'magnite.com'] } // make sure we only pass max 10 adomains bidResponse2.meta.advertiserDomains = [...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains] events.emit(BID_RESPONSE, bidResponse1); events.emit(BID_RESPONSE, bidResponse2); events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); events.emit(AUCTION_END, MOCK.AUCTION_END); events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[0]); events.emit(BID_WON, MOCK.BID_WON[1]); let message = JSON.parse(server.requests[0].requestBody); validate(message); expect(message.auctions[0].adUnits[0].bids[0].bidResponse.adomains).to.deep.equal(['magnite.com']); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.adomains).to.deep.equal(['prebid.org', 'magnite.com', 'prebid.org', 'magnite.com', 'prebid.org', 'magnite.com', 'prebid.org', 'magnite.com', 'prebid.org', 'magnite.com']); }); it('should NOT pass along adomians correctly when edge cases', function () { events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); // empty => nothing let bidResponse1 = utils.deepClone(MOCK.BID_RESPONSE[0]); bidResponse1.meta = { advertiserDomains: [] } // not array => nothing let bidResponse2 = utils.deepClone(MOCK.BID_RESPONSE[1]); bidResponse2.meta = { advertiserDomains: 'prebid.org' } events.emit(BID_RESPONSE, bidResponse1); events.emit(BID_RESPONSE, bidResponse2); events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); events.emit(AUCTION_END, MOCK.AUCTION_END); events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[0]); events.emit(BID_WON, MOCK.BID_WON[1]); let message = JSON.parse(server.requests[0].requestBody); validate(message); expect(message.auctions[0].adUnits[0].bids[0].bidResponse.adomains).to.be.undefined; expect(message.auctions[0].adUnits[1].bids[0].bidResponse.adomains).to.be.undefined; }); function performFloorAuction(provider) { let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); auctionInit.bidderRequests[0].bids[0].floorData = { skipped: false, modelVersion: 'someModelName', modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 15, fetchStatus: 'error', floorProvider: provider }; let flooredResponse = { ...BID, floorData: { floorValue: 4, floorRule: '12345/sports|video', floorCurrency: 'USD', cpmAfterAdjustments: 2.1, enforcements: { enforceJS: true, enforcePBS: false, floorDeals: false, bidAdjustment: true }, matchedFields: { gptSlot: '12345/sports', mediaType: 'video' } }, status: 'bidRejected', cpm: 0, getStatusCode() { return 2; } }; let notFlooredResponse = { ...BID2, floorData: { floorValue: 1, floorRule: '12345/news|banner', floorCurrency: 'USD', cpmAfterAdjustments: 1.55, enforcements: { enforceJS: true, enforcePBS: false, floorDeals: false, bidAdjustment: true }, matchedFields: { gptSlot: '12345/news', mediaType: 'banner' } } }; let floorMinResponse = { ...BID3, floorData: { floorValue: 1.5, floorRuleValue: 1, floorRule: '12345/entertainment|banner', floorCurrency: 'USD', cpmAfterAdjustments: 2.00, enforcements: { enforceJS: true, enforcePBS: false, floorDeals: false, bidAdjustment: true }, matchedFields: { gptSlot: '12345/entertainment', mediaType: 'banner' } } }; let bidRequest = utils.deepClone(MOCK.BID_REQUESTED); bidRequest.bids.push(floorMinRequest) // spoof the auction with just our duplicates events.emit(AUCTION_INIT, auctionInit); events.emit(BID_REQUESTED, bidRequest); events.emit(BID_RESPONSE, flooredResponse); events.emit(BID_RESPONSE, notFlooredResponse); events.emit(BID_RESPONSE, floorMinResponse); events.emit(AUCTION_END, MOCK.AUCTION_END); events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[1]); events.emit(BID_WON, MOCK.BID_WON[2]); clock.tick(SEND_TIMEOUT + 1000); expect(server.requests.length).to.equal(1); let message = JSON.parse(server.requests[0].requestBody); validate(message); return message; } it('should capture price floor information correctly', function () { let message = performFloorAuction('rubicon'); // verify our floor stuff is passed // top level floor info expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', modelWeight: 10, modelTimestamp: 1606772895, skipped: false, enforcement: true, dealsEnforced: false, skipRate: 15, fetchStatus: 'error', provider: 'rubicon' }); // first adUnit's adSlot expect(message.auctions[0].adUnits[0].gam.adSlot).to.equal('12345/sports'); // since no other bids, we set adUnit status to no-bid expect(message.auctions[0].adUnits[0].status).to.equal('no-bid'); // first adUnits bid is rejected expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected-ipf'); expect(message.auctions[0].adUnits[0].bids[0].bidResponse.floorValue).to.equal(4); // if bid rejected should take cpmAfterAdjustments val expect(message.auctions[0].adUnits[0].bids[0].bidResponse.bidPriceUSD).to.equal(2.1); // second adUnit's adSlot expect(message.auctions[0].adUnits[1].gam.adSlot).to.equal('12345/news'); // top level adUnit status is success expect(message.auctions[0].adUnits[1].status).to.equal('success'); // second adUnits bid is success expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); // second adUnit's adSlot expect(message.auctions[0].adUnits[2].gam.adSlot).to.equal('12345/entertainment'); // top level adUnit status is success expect(message.auctions[0].adUnits[2].status).to.equal('success'); // second adUnits bid is success expect(message.auctions[0].adUnits[2].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorValue).to.equal(1.5); expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorRuleValue).to.equal(1); expect(message.auctions[0].adUnits[2].bids[0].bidResponse.bidPriceUSD).to.equal(2.01); }); it('should still send floor info if provider is not rubicon', function () { let message = performFloorAuction('randomProvider'); // verify our floor stuff is passed // top level floor info expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', modelWeight: 10, modelTimestamp: 1606772895, skipped: false, enforcement: true, dealsEnforced: false, skipRate: 15, fetchStatus: 'error', provider: 'randomProvider' }); // first adUnit's adSlot expect(message.auctions[0].adUnits[0].gam.adSlot).to.equal('12345/sports'); // since no other bids, we set adUnit status to no-bid expect(message.auctions[0].adUnits[0].status).to.equal('no-bid'); // first adUnits bid is rejected expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected-ipf'); expect(message.auctions[0].adUnits[0].bids[0].bidResponse.floorValue).to.equal(4); // if bid rejected should take cpmAfterAdjustments val expect(message.auctions[0].adUnits[0].bids[0].bidResponse.bidPriceUSD).to.equal(2.1); // second adUnit's adSlot expect(message.auctions[0].adUnits[1].gam.adSlot).to.equal('12345/news'); // top level adUnit status is success expect(message.auctions[0].adUnits[1].status).to.equal('success'); // second adUnits bid is success expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); // second adUnit's adSlot expect(message.auctions[0].adUnits[2].gam.adSlot).to.equal('12345/entertainment'); // top level adUnit status is success expect(message.auctions[0].adUnits[2].status).to.equal('success'); // second adUnits bid is success expect(message.auctions[0].adUnits[2].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorValue).to.equal(1.5); expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorRuleValue).to.equal(1); expect(message.auctions[0].adUnits[2].bids[0].bidResponse.bidPriceUSD).to.equal(2.01); }); describe('with session handling', function () { const expectedPvid = STUBBED_UUID.slice(0, 8); beforeEach(function () { config.setConfig({rubicon: {updatePageView: true}}); }); it('should not log any session data if local storage is not enabled', function () { localStorageIsEnabledStub.returns(false); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); delete expectedMessage.session; delete expectedMessage.fpkvs; performStandardAuction(); expect(server.requests.length).to.equal(1); let request = server.requests[0]; expect(request.url).to.equal('//localhost:9999/event'); let message = JSON.parse(request.requestBody); validate(message); expect(message).to.deep.equal(expectedMessage); }); it('should should pass along custom rubicon kv and pvid when defined', function () { config.setConfig({rubicon: { fpkvs: { source: 'fb', link: 'email' } }}); performStandardAuction(); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); expectedMessage.session.pvid = STUBBED_UUID.slice(0, 8); expectedMessage.fpkvs = [ {key: 'source', value: 'fb'}, {key: 'link', value: 'email'} ] expect(message).to.deep.equal(expectedMessage); }); it('should convert kvs to strings before sending', function () { config.setConfig({rubicon: { fpkvs: { number: 24, boolean: false, string: 'hello', array: ['one', 2, 'three'], object: {one: 'two'} } }}); performStandardAuction(); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); expectedMessage.session.pvid = STUBBED_UUID.slice(0, 8); expectedMessage.fpkvs = [ {key: 'number', value: '24'}, {key: 'boolean', value: 'false'}, {key: 'string', value: 'hello'}, {key: 'array', value: 'one,2,three'}, {key: 'object', value: '[object Object]'} ] expect(message).to.deep.equal(expectedMessage); }); it('should use the query utm param rubicon kv value and pass updated kv and pvid when defined', function () { sandbox.stub(utils, 'getWindowLocation').returns({'search': '?utm_source=other', 'pbjs_debug': 'true'}); config.setConfig({rubicon: { fpkvs: { source: 'fb', link: 'email' } }}); performStandardAuction(); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); expectedMessage.session.pvid = STUBBED_UUID.slice(0, 8); expectedMessage.fpkvs = [ {key: 'source', value: 'other'}, {key: 'link', value: 'email'} ] message.fpkvs.sort((left, right) => left.key < right.key); expectedMessage.fpkvs.sort((left, right) => left.key < right.key); expect(message).to.deep.equal(expectedMessage); }); it('should pick up existing localStorage and use its values', function () { // set some localStorage let inputlocalStorage = { id: '987654', start: 1519766113781, // 15 mins before "now" expires: 1519787713781, // six hours later lastSeen: 1519766113781, fpkvs: { source: 'tw' } }; getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); config.setConfig({rubicon: { fpkvs: { link: 'email' // should merge this with what is in the localStorage! } }}); performStandardAuction(); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); expectedMessage.session = { id: '987654', start: 1519766113781, expires: 1519787713781, pvid: expectedPvid } expectedMessage.fpkvs = [ {key: 'source', value: 'tw'}, {key: 'link', value: 'email'} ] expect(message).to.deep.equal(expectedMessage); let calledWith; try { calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); } catch (e) { calledWith = {}; } expect(calledWith).to.deep.equal({ id: '987654', // should have stayed same start: 1519766113781, // should have stayed same expires: 1519787713781, // should have stayed same lastSeen: 1519767013781, // lastSeen updated to our "now" fpkvs: { source: 'tw', link: 'email' }, // link merged in pvid: expectedPvid // new pvid stored }); }); it('should overwrite matching localstorge value and use its remaining values', function () { sandbox.stub(utils, 'getWindowLocation').returns({'search': '?utm_source=fb&utm_click=dog'}); // set some localStorage let inputlocalStorage = { id: '987654', start: 1519766113781, // 15 mins before "now" expires: 1519787713781, // six hours later lastSeen: 1519766113781, fpkvs: { source: 'tw', link: 'email' } }; getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); config.setConfig({rubicon: { fpkvs: { link: 'email' // should merge this with what is in the localStorage! } }}); performStandardAuction(); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); expectedMessage.session = { id: '987654', start: 1519766113781, expires: 1519787713781, pvid: expectedPvid } expectedMessage.fpkvs = [ {key: 'source', value: 'fb'}, {key: 'link', value: 'email'}, {key: 'click', value: 'dog'} ] message.fpkvs.sort((left, right) => left.key < right.key); expectedMessage.fpkvs.sort((left, right) => left.key < right.key); expect(message).to.deep.equal(expectedMessage); let calledWith; try { calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); } catch (e) { calledWith = {}; } expect(calledWith).to.deep.equal({ id: '987654', // should have stayed same start: 1519766113781, // should have stayed same expires: 1519787713781, // should have stayed same lastSeen: 1519767013781, // lastSeen updated to our "now" fpkvs: { source: 'fb', link: 'email', click: 'dog' }, // link merged in pvid: expectedPvid // new pvid stored }); }); it('should throw out session if lastSeen > 30 mins ago and create new one', function () { // set some localStorage let inputlocalStorage = { id: '987654', start: 1519764313781, // 45 mins before "now" expires: 1519785913781, // six hours later lastSeen: 1519764313781, // 45 mins before "now" fpkvs: { source: 'tw' } }; getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); config.setConfig({rubicon: { fpkvs: { link: 'email' // should merge this with what is in the localStorage! } }}); performStandardAuction(); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); // session should match what is already in ANALYTICS_MESSAGE, just need to add pvid expectedMessage.session.pvid = expectedPvid; // the saved fpkvs should have been thrown out since session expired expectedMessage.fpkvs = [ {key: 'link', value: 'email'} ] expect(message).to.deep.equal(expectedMessage); let calledWith; try { calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); } catch (e) { calledWith = {}; } expect(calledWith).to.deep.equal({ id: STUBBED_UUID, // should have stayed same start: 1519767013781, // should have stayed same expires: 1519788613781, // should have stayed same lastSeen: 1519767013781, // lastSeen updated to our "now" fpkvs: { link: 'email' }, // link merged in pvid: expectedPvid // new pvid stored }); }); it('should throw out session if past expires time and create new one', function () { // set some localStorage let inputlocalStorage = { id: '987654', start: 1519745353781, // 6 hours before "expires" expires: 1519766953781, // little more than six hours ago lastSeen: 1519767008781, // 5 seconds ago fpkvs: { source: 'tw' } }; getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); config.setConfig({rubicon: { fpkvs: { link: 'email' // should merge this with what is in the localStorage! } }}); performStandardAuction(); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); // session should match what is already in ANALYTICS_MESSAGE, just need to add pvid expectedMessage.session.pvid = expectedPvid; // the saved fpkvs should have been thrown out since session expired expectedMessage.fpkvs = [ {key: 'link', value: 'email'} ] expect(message).to.deep.equal(expectedMessage); let calledWith; try { calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); } catch (e) { calledWith = {}; } expect(calledWith).to.deep.equal({ id: STUBBED_UUID, // should have stayed same start: 1519767013781, // should have stayed same expires: 1519788613781, // should have stayed same lastSeen: 1519767013781, // lastSeen updated to our "now" fpkvs: { link: 'email' }, // link merged in pvid: expectedPvid // new pvid stored }); }); }); describe('with googletag enabled', function () { let gptSlot0, gptSlot1; let gptSlotRenderEnded0, gptSlotRenderEnded1; beforeEach(function () { mockGpt.enable(); gptSlot0 = mockGpt.makeSlot({code: '/19968336/header-bid-tag-0'}); gptSlotRenderEnded0 = { eventName: 'slotRenderEnded', params: { slot: gptSlot0, isEmpty: false, advertiserId: 1111, sourceAgnosticCreativeId: 2222, sourceAgnosticLineItemId: 3333 } }; gptSlot1 = mockGpt.makeSlot({code: '/19968336/header-bid-tag1'}); gptSlotRenderEnded1 = { eventName: 'slotRenderEnded', params: { slot: gptSlot1, isEmpty: false, advertiserId: 4444, sourceAgnosticCreativeId: 5555, sourceAgnosticLineItemId: 6666 } }; }); afterEach(function () { mockGpt.disable(); }); it('should add necessary gam information if gpt is enabled and slotRender event emmited', function () { performStandardAuction([gptSlotRenderEnded0, gptSlotRenderEnded1]); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); expectedMessage.auctions[0].adUnits[0].gam = { advertiserId: 1111, creativeId: 2222, lineItemId: 3333, adSlot: '/19968336/header-bid-tag-0' }; expectedMessage.auctions[0].adUnits[1].gam = { advertiserId: 4444, creativeId: 5555, lineItemId: 6666, adSlot: '/19968336/header-bid-tag1' }; expect(message).to.deep.equal(expectedMessage); }); it('should handle empty gam renders', function () { performStandardAuction([gptSlotRenderEnded0, { eventName: 'slotRenderEnded', params: { slot: gptSlot1, isEmpty: true } }]); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); expectedMessage.auctions[0].adUnits[0].gam = { advertiserId: 1111, creativeId: 2222, lineItemId: 3333, adSlot: '/19968336/header-bid-tag-0' }; expectedMessage.auctions[0].adUnits[1].gam = { isSlotEmpty: true, adSlot: '/19968336/header-bid-tag1' }; expect(message).to.deep.equal(expectedMessage); }); it('should still add gam ids if falsy', function () { performStandardAuction([gptSlotRenderEnded0, { eventName: 'slotRenderEnded', params: { slot: gptSlot1, isEmpty: false, advertiserId: 0, sourceAgnosticCreativeId: 0, source