From cbdea4ab2b786c7bf4796a72840f20b574752b41 Mon Sep 17 00:00:00 2001 From: Udi Talias Date: Sat, 21 Mar 2020 01:33:55 +0200 Subject: [PATCH 01/90] Vidazoo adapter: update maintainer email (#5006) --- modules/vidazooBidAdapter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/vidazooBidAdapter.md b/modules/vidazooBidAdapter.md index 1e9dc47dd51..df7700cb05a 100644 --- a/modules/vidazooBidAdapter.md +++ b/modules/vidazooBidAdapter.md @@ -4,7 +4,7 @@ **Module Type:** Bidder Adapter -**Maintainer:** server-dev@getintent.com +**Maintainer:** dev@vidazoo.com # Description From dfcbd2587591efabf6c12696e518159cccfcf6af Mon Sep 17 00:00:00 2001 From: weilingxi-qc <62362124+weilingxi-qc@users.noreply.github.com> Date: Fri, 20 Mar 2020 16:50:19 -0700 Subject: [PATCH 02/90] Add coppa support for Quantcast adapter (#5012) * add coppa support in quantcast adapter * fix unit tests --- modules/quantcastBidAdapter.js | 1 + test/spec/modules/quantcastBidAdapter_spec.js | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index bcd6929cd04..4ffb84b8934 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -141,6 +141,7 @@ export const spec = { gdprConsent: gdprConsent.consentString, uspSignal: uspConsent ? 1 : 0, uspConsent, + coppa: config.getConfig('coppa') === true ? 1 : 0, prebidJsVersion: '$prebid.version$' }; diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index 19d53674793..351cea0b086 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -11,6 +11,7 @@ import { } from '../../../modules/quantcastBidAdapter.js'; import { newBidder } from '../../../src/adapters/bidderFactory.js'; import { parse } from 'src/url.js'; +import { config } from 'src/config.js'; describe('Quantcast adapter', function () { const quantcastAdapter = newBidder(qcSpec); @@ -138,6 +139,7 @@ describe('Quantcast adapter', function () { bidId: '2f7b179d443f14', gdprSignal: 0, uspSignal: 0, + coppa: 0, prebidJsVersion: '$prebid.version$' }; @@ -205,6 +207,7 @@ describe('Quantcast adapter', function () { bidId: '2f7b179d443f14', gdprSignal: 0, uspSignal: 0, + coppa: 0, prebidJsVersion: '$prebid.version$' }; @@ -240,6 +243,7 @@ describe('Quantcast adapter', function () { bidId: '2f7b179d443f14', gdprSignal: 0, uspSignal: 0, + coppa: 0, prebidJsVersion: '$prebid.version$' }; @@ -271,6 +275,7 @@ describe('Quantcast adapter', function () { bidId: '2f7b179d443f14', gdprSignal: 0, uspSignal: 0, + coppa: 0, prebidJsVersion: '$prebid.version$' }; @@ -334,6 +339,7 @@ describe('Quantcast adapter', function () { bidId: '2f7b179d443f14', gdprSignal: 0, uspSignal: 0, + coppa: 0, prebidJsVersion: '$prebid.version$' }; @@ -357,6 +363,50 @@ describe('Quantcast adapter', function () { expect(parsed.uspConsent).to.equal('consentString'); }); + describe('propagates coppa', function() { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('propagates coppa as 1 if coppa param is set to true in the bid request', function () { + bidRequest.params = { + publisherId: 'test_publisher_id', + coppa: true + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + const config = { + 'coppa': true + }; + return config[key]; + }); + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + expect(JSON.parse(requests[0].data).coppa).to.equal(1); + }); + + it('propagates coppa as 0 if there is no coppa param or coppa is set to false in the bid request', function () { + const requestsWithoutCoppa = qcSpec.buildRequests([bidRequest], bidderRequest); + expect(JSON.parse(requestsWithoutCoppa[0].data).coppa).to.equal(0); + + bidRequest.params = { + publisherId: 'test_publisher_id', + coppa: false + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + const config = { + 'coppa': false + }; + return config[key]; + }); + const requestsWithFalseCoppa = qcSpec.buildRequests([bidRequest], bidderRequest); + expect(JSON.parse(requestsWithFalseCoppa[0].data).coppa).to.equal(0); + }); + }); + describe('`interpretResponse`', function () { // The sample response is from https://wiki.corp.qc/display/adinf/QCX const body = { From 9523787f87ec9c785ddc4532e7a845acaac7f557 Mon Sep 17 00:00:00 2001 From: thuyhq <61451682+thuyhq@users.noreply.github.com> Date: Sat, 21 Mar 2020 07:16:19 +0700 Subject: [PATCH 03/90] Add Valueimpression Bid Adapter (#4974) * Add Valueimpression Bid Adapter * Update valueimpressionBidAdapter_spec.js * Update valueimpressionBidAdapter.md * Lint valueimpressionBidAdapter.js * Update valueimpressionBidAdapter.js --- modules/valueimpressionBidAdapter.js | 152 +++++ modules/valueimpressionBidAdapter.md | 56 ++ .../modules/valueimpressionBidAdapter_spec.js | 602 ++++++++++++++++++ 3 files changed, 810 insertions(+) create mode 100644 modules/valueimpressionBidAdapter.js create mode 100644 modules/valueimpressionBidAdapter.md create mode 100644 test/spec/modules/valueimpressionBidAdapter_spec.js diff --git a/modules/valueimpressionBidAdapter.js b/modules/valueimpressionBidAdapter.js new file mode 100644 index 00000000000..41bb6e6cacc --- /dev/null +++ b/modules/valueimpressionBidAdapter.js @@ -0,0 +1,152 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +const BIDDER_CODE = 'valueimpression'; +const ENDPOINT = 'https://adapter.valueimpression.com/bid'; +const USER_SYNC_URL = 'https://adapter.valueimpression.com/usersync'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['banner', 'video'], + aliases: ['vi'], + isBidRequestValid: function (bid) { + if (!bid.params) { + return false; + } + if (!bid.params.siteId) { + return false; + } + if (!utils.deepAccess(bid, 'mediaTypes.banner') && !utils.deepAccess(bid, 'mediaTypes.video')) { + return false; + } + if (utils.deepAccess(bid, 'mediaTypes.banner')) { // Valueimpression does not support multi type bids, favor banner over video + if (!utils.deepAccess(bid, 'mediaTypes.banner.sizes')) { + // sizes at the banner is required. + return false; + } + } else if (utils.deepAccess(bid, 'mediaTypes.video')) { + if (!utils.deepAccess(bid, 'mediaTypes.video.playerSize')) { + // playerSize is required for instream adUnits. + return false; + } + } + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const payload = {}; + payload.device = {}; + payload.device.ua = navigator.userAgent; + payload.device.height = window.innerHeight; + payload.device.width = window.innerWidth; + payload.device.dnt = _getDoNotTrack(); + payload.device.language = navigator.language; + + payload.site = {}; + payload.site.id = validBidRequests[0].params.siteId; + payload.site.page = window.location.href; + payload.site.referrer = document.referrer; + payload.site.hostname = window.location.hostname; + + // Apply GDPR parameters to request. + payload.gdpr = {}; + if (bidderRequest && bidderRequest.gdprConsent) { + payload.gdpr.gdprApplies = bidderRequest.gdprConsent.gdprApplies ? 'true' : 'false'; + if (bidderRequest.gdprConsent.consentString) { + payload.gdpr.consentString = bidderRequest.gdprConsent.consentString; + } + } + if (validBidRequests[0].schain) { + payload.schain = JSON.stringify(validBidRequests[0].schain) + } + if (bidderRequest && bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } + + payload.bids = validBidRequests; + + return { + method: 'POST', + url: ENDPOINT, + data: payload, + withCredentials: true, + bidderRequests: validBidRequests + }; + }, + interpretResponse: function (serverResponse, bidRequest) { + const serverBody = serverResponse.body; + const serverBids = serverBody.bids; + // check overall response + if (!serverBody || typeof serverBody !== 'object') { + return []; + } + if (!serverBids || typeof serverBids !== 'object') { + return []; + } + + const bidResponses = []; + serverBids.forEach(bid => { + const bidResponse = { + requestId: bid.requestId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.creativeId, + dealId: bid.dealId, + currency: bid.currency, + netRevenue: bid.netRevenue, + ttl: bid.ttl, + mediaType: bid.mediaType + }; + if (bid.vastXml) { + bidResponse.vastXml = utils.replaceAuctionPrice(bid.vastXml, bid.cpm); + } else { + bidResponse.ad = utils.replaceAuctionPrice(bid.ad, bid.cpm); + } + bidResponses.push(bidResponse); + }); + return bidResponses; + }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + try { + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: USER_SYNC_URL + }); + } + if (syncOptions.pixelEnabled && serverResponses.length > 0) { + serverResponses[0].body.pixel.forEach(px => { + syncs.push({ + type: px.type, + url: px.url + }); + }); + } + } catch (e) { } + return syncs; + }, + + onTimeout: function (timeoutData) { + }, + + onBidWon: function (bid) { + }, + + onSetTargeting: function (bid) { + } +}; + +function _getDoNotTrack() { + if (window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack) { + if (window.doNotTrack == '1' || navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') { + return 1; + } else { + return 0; + } + } else { + return 0; + } +} + +registerBidder(spec); diff --git a/modules/valueimpressionBidAdapter.md b/modules/valueimpressionBidAdapter.md new file mode 100644 index 00000000000..11400f23834 --- /dev/null +++ b/modules/valueimpressionBidAdapter.md @@ -0,0 +1,56 @@ +# Overview + +``` +Module Name: Valueimpression Bidder Adapter +Module Type: Bidder Adapter +Maintainer: thuyhq@83.com.vn +``` + +# Description + +Module that connects to Valueimpression's exchange for bids. +Valueimpression Bidder adapter supports Banner and Video ads. + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [ + { + bidder: 'valueimpression', + params: { + siteId: 'vi-site-id', // siteId provided by Valueimpression + } + } + ] + } +]; +``` + +# Video Test Parameters +``` +var videoAdUnit = { + code: 'test-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + }, + }, + bids: [ + { + bidder: 'valueimpression', + params: { + siteId: 'vi-site-id', // siteId provided by Valueimpression + } + } + ] +}; +``` \ No newline at end of file diff --git a/test/spec/modules/valueimpressionBidAdapter_spec.js b/test/spec/modules/valueimpressionBidAdapter_spec.js new file mode 100644 index 00000000000..89a9657aff4 --- /dev/null +++ b/test/spec/modules/valueimpressionBidAdapter_spec.js @@ -0,0 +1,602 @@ +import { expect } from 'chai' +import { spec } from 'modules/valueimpressionBidAdapter.js' +import { newBidder } from 'src/adapters/bidderFactory.js' +import { userSync } from '../../../src/userSync.js'; + +describe('ValueimpressionBidAdapter', function () { + const adapter = newBidder(spec) + + describe('.code', function () { + it('should return a bidder code of valueimpression', function () { + expect(spec.code).to.equal('valueimpression') + }) + }) + + describe('inherited functions', function () { + it('should exist and be a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function') + }) + }) + + describe('.isBidRequestValid', function () { + it('should return false if there are no params', () => { + const bid = { + 'bidder': 'valueimpression', + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if there is no siteId param', () => { + const bid = { + 'bidder': 'valueimpression', + 'adUnitCode': 'adunit-code', + params: { + site_id: '1a2b3c4d5e6f1a2b3c4d', + }, + 'mediaTypes': { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if there is no mediaTypes', () => { + const bid = { + 'bidder': 'valueimpression', + 'adUnitCode': 'adunit-code', + params: { + siteId: '1a2b3c4d5e6f1a2b3c4d' + }, + 'mediaTypes': { + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true if the bid is valid', () => { + const bid = { + 'bidder': 'valueimpression', + 'adUnitCode': 'adunit-code', + params: { + siteId: '1a2b3c4d5e6f1a2b3c4d' + }, + 'mediaTypes': { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + describe('banner', () => { + it('should return false if there are no banner sizes', () => { + const bid = { + 'bidder': 'valueimpression', + 'adUnitCode': 'adunit-code', + params: { + siteId: '1a2b3c4d5e6f1a2b3c4d' + }, + 'mediaTypes': { + banner: { + + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true if there is banner sizes', () => { + const bid = { + 'bidder': 'valueimpression', + 'adUnitCode': 'adunit-code', + params: { + siteId: '1a2b3c4d5e6f1a2b3c4d' + }, + 'mediaTypes': { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('video', () => { + it('should return false if there is no playerSize defined in the video mediaType', () => { + const bid = { + 'bidder': 'valueimpression', + 'adUnitCode': 'adunit-code', + params: { + siteId: '1a2b3c4d5e6f1a2b3c4d', + sizes: [[300, 250], [300, 600]] + }, + 'mediaTypes': { + video: { + context: 'instream' + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true if there is playerSize defined on the video mediaType', () => { + const bid = { + 'bidder': 'valueimpression', + 'adUnitCode': 'adunit-code', + params: { + siteId: '1a2b3c4d5e6f1a2b3c4d', + }, + 'mediaTypes': { + video: { + context: 'instream', + playerSize: [[640, 480]] + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + }); + + describe('.buildRequests', function () { + beforeEach(function () { + sinon.stub(userSync, 'canBidderRegisterSync'); + }); + afterEach(function () { + userSync.canBidderRegisterSync.restore(); + }); + let bidRequest = [{ + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'indirectseller.com', + 'sid': '00001', + 'hp': 1 + }, + { + 'asi': 'indirectseller-2.com', + 'sid': '00002', + 'hp': 0 + }, + ] + }, + 'bidder': 'valueimpression', + 'params': { + 'siteId': '1a2b3c4d5e6f1a2b3c4d', + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1f', + }, + { + 'bidder': 'valueimpression', + 'params': { + 'ad_unit': '/7780971/sparks_prebid_LB', + 'sizes': [[300, 250], [300, 600]], + 'referrer': 'overrides_top_window_location' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[120, 600], [300, 600], [160, 600]], + 'bidId': '30b31c1838de1e', + }]; + + let bidderRequests = { + 'gdprConsent': { + 'consentString': 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + 'vendorData': {}, + 'gdprApplies': true + }, + 'refererInfo': { + 'numIframes': 0, + 'reachedTop': true, + 'referer': 'https://example.com', + 'stack': ['https://example.com'] + }, + uspConsent: 'someCCPAString' + }; + + it('should return a properly formatted request', function () { + const bidRequests = spec.buildRequests(bidRequest, bidderRequests) + expect(bidRequests.url).to.equal('https://adapter.valueimpression.com/bid') + expect(bidRequests.method).to.equal('POST') + expect(bidRequests.bidderRequests).to.eql(bidRequest); + }) + + it('should return a properly formatted request with GDPR applies set to true', function () { + const bidRequests = spec.buildRequests(bidRequest, bidderRequests) + expect(bidRequests.url).to.equal('https://adapter.valueimpression.com/bid') + expect(bidRequests.method).to.equal('POST') + expect(bidRequests.data.gdpr.gdprApplies).to.equal('true') + expect(bidRequests.data.gdpr.consentString).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A==') + }) + + it('should return a properly formatted request with GDPR applies set to false', function () { + bidderRequests.gdprConsent.gdprApplies = false; + const bidRequests = spec.buildRequests(bidRequest, bidderRequests) + expect(bidRequests.url).to.equal('https://adapter.valueimpression.com/bid') + expect(bidRequests.method).to.equal('POST') + expect(bidRequests.data.gdpr.gdprApplies).to.equal('false') + expect(bidRequests.data.gdpr.consentString).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A==') + }) + it('should return a properly formatted request with GDPR applies set to false with no consent_string param', function () { + let bidderRequests = { + 'gdprConsent': { + 'consentString': undefined, + 'vendorData': {}, + 'gdprApplies': false + }, + 'refererInfo': { + 'numIframes': 0, + 'reachedTop': true, + 'referer': 'https://example.com', + 'stack': ['https://example.com'] + } + }; + const bidRequests = spec.buildRequests(bidRequest, bidderRequests) + expect(bidRequests.url).to.equal('https://adapter.valueimpression.com/bid') + expect(bidRequests.method).to.equal('POST') + expect(bidRequests.data.gdpr.gdprApplies).to.equal('false') + expect(bidRequests.data.gdpr).to.not.include.keys('consentString') + }) + it('should return a properly formatted request with GDPR applies set to true with no consentString param', function () { + let bidderRequests = { + 'gdprConsent': { + 'consentString': undefined, + 'vendorData': {}, + 'gdprApplies': true + }, + 'refererInfo': { + 'numIframes': 0, + 'reachedTop': true, + 'referer': 'https://example.com', + 'stack': ['https://example.com'] + } + }; + const bidRequests = spec.buildRequests(bidRequest, bidderRequests) + expect(bidRequests.url).to.equal('https://adapter.valueimpression.com/bid') + expect(bidRequests.method).to.equal('POST') + expect(bidRequests.data.gdpr.gdprApplies).to.equal('true') + expect(bidRequests.data.gdpr).to.not.include.keys('consentString') + }) + it('should return a properly formatted request with schain defined', function () { + const bidRequests = spec.buildRequests(bidRequest, bidderRequests); + expect(JSON.parse(bidRequests.data.schain)).to.deep.equal(bidRequest[0].schain) + }); + it('should return a properly formatted request with us_privacy included', function () { + const bidRequests = spec.buildRequests(bidRequest, bidderRequests); + expect(bidRequests.data.us_privacy).to.equal('someCCPAString'); + }); + }); + + describe('.interpretResponse', function () { + const bidRequests = { + 'method': 'POST', + 'url': 'https://adapter.valueimpression.com/bid', + 'withCredentials': true, + 'data': { + 'device': { + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36', + 'height': 937, + 'width': 1920, + 'dnt': 0, + 'language': 'vi' + }, + 'site': { + 'id': '343', + 'page': 'https://www.includehelp.com/?pbjs_debug=true', + 'referrer': '', + 'hostname': 'www.includehelp.com' + } + }, + 'bidderRequests': [ + { + 'bidder': 'valueimpression', + 'params': { + 'siteId': '343' + }, + 'crumbs': { + 'pubcid': 'c2b2ba08-9954-4850-8ee5-2bf4a2b35eff' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[160, 600], [120, 600]] + } + }, + 'adUnitCode': 'vi_3431035_1', + 'transactionId': '994d8404-4a28-4c43-98d8-a38dbb061910', + 'sizes': [[160, 600], [120, 600]], + 'bidId': '3000aa31c41a29c21', + 'bidderRequestId': '299926b3d3628cdd7', + 'auctionId': '22445943-a0aa-4c63-a413-4deb64fcff1c', + 'src': 'client', + 'bidRequestsCount': 41, + 'bidderRequestsCount': 41, + 'bidderWinsCount': 0, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'freegames66.com', + 'sid': '343', + 'hp': 1 + } + ] + } + }, + { + 'bidder': 'valueimpression', + 'params': { + 'siteId': '343' + }, + 'crumbs': { + 'pubcid': 'c2b2ba08-9954-4850-8ee5-2bf4a2b35eff' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [250, 250], [200, 200], [180, 150]] + } + }, + 'adUnitCode': 'vi_3431033_1', + 'transactionId': '800fe87e-bfec-43a5-ace0-c4e2373ff4b5', + 'sizes': [[300, 250], [250, 250], [200, 200], [180, 150]], + 'bidId': '30024615be22ef66a', + 'bidderRequestId': '299926b3d3628cdd7', + 'auctionId': '22445943-a0aa-4c63-a413-4deb64fcff1c', + 'src': 'client', + 'bidRequestsCount': 41, + 'bidderRequestsCount': 41, + 'bidderWinsCount': 0, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'freegames66.com', + 'sid': '343', + 'hp': 1 + } + ] + } + }, + { + 'bidder': 'valueimpression', + 'params': { + 'siteId': '343' + }, + 'crumbs': { + 'pubcid': 'c2b2ba08-9954-4850-8ee5-2bf4a2b35eff' + }, + 'mediaTypes': { + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream', + 'mimes': [ + 'video/mp4', + 'video/x-flv', + 'video/x-ms-wmv', + 'application/vnd.apple.mpegurl', + 'application/x-mpegurl', + 'video/3gpp', + 'video/mpeg', + 'video/ogg', + 'video/quicktime', + 'video/webm', + 'video/x-m4v', + 'video/ms-asf', + 'video/x-msvideo' + ], + 'protocols': [1, 2, 3, 4, 5, 6], + 'playbackmethod': [6], + 'maxduration': 120, + 'linearity': 1, + 'api': [2] + } + }, + 'adUnitCode': 'vi_3431909', + 'transactionId': '33d83d87-43cc-499b-aabe-5c22eb6acfbb', + 'sizes': [[640, 480]], + 'bidId': '1854b40107d6745c', + 'bidderRequestId': '1840763b6bda185d', + 'auctionId': 'df495de0-5d42-471f-a501-73bcd7254b80', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'freegames66.com', + 'sid': '343', + 'hp': 1 + } + ] + } + } + ] + }; + + let serverResponse = { + 'body': { + 'bids': [ + { + 'requestId': '3000aa31c41a29c21', + 'cpm': 1.07, + 'width': 160, + 'height': 600, + 'ad': `
Valueimpression AD
`, + 'ttl': 500, + 'creativeId': '1234abcd', + 'netRevenue': true, + 'currency': 'USD', + 'dealId': 'valueimpression', + 'mediaType': 'banner' + }, + { + 'requestId': '30024615be22ef66a', + 'cpm': 1, + 'width': 300, + 'height': 250, + 'ad': `
Valueimpression AD
`, + 'ttl': 500, + 'creativeId': '1234abcd', + 'netRevenue': true, + 'currency': 'USD', + 'dealId': 'valueimpression', + 'mediaType': 'banner' + }, + { + 'requestId': '1854b40107d6745c', + 'cpm': 1.25, + 'width': 300, + 'height': 250, + 'vastXml': 'valueimpression', + 'ttl': 500, + 'creativeId': '30292e432662bd5f86d90774b944b038', + 'netRevenue': true, + 'currency': 'USD', + 'dealId': 'valueimpression', + 'mediaType': 'video' + } + ], + 'pixel': [{ + 'url': 'https://example.com/pixel.png', + 'type': 'image' + }] + } + }; + + let prebidResponse = [ + { + 'requestId': '3000aa31c41a29c21', + 'cpm': 1.07, + 'width': 160, + 'height': 600, + 'ad': `
Valueimpression AD
`, + 'ttl': 500, + 'creativeId': '1234abcd', + 'netRevenue': true, + 'currency': 'USD', + 'dealId': 'valueimpression', + 'mediaType': 'banner' + }, + { + 'requestId': '30024615be22ef66a', + 'cpm': 1, + 'width': 300, + 'height': 250, + 'ad': `
Valueimpression AD
`, + 'ttl': 500, + 'creativeId': '1234abcd', + 'netRevenue': true, + 'currency': 'USD', + 'dealId': 'valueimpression', + 'mediaType': 'banner' + }, + { + 'requestId': '1854b40107d6745c', + 'cpm': 1.25, + 'width': 300, + 'height': 250, + 'vastXml': 'valueimpression', + 'ttl': 500, + 'creativeId': '30292e432662bd5f86d90774b944b038', + 'netRevenue': true, + 'currency': 'USD', + 'dealId': 'valueimpression', + 'mediaType': 'video' + } + ]; + + it('should map bidResponse to prebidResponse', function () { + const response = spec.interpretResponse(serverResponse, bidRequests); + response.forEach((resp, i) => { + expect(resp.requestId).to.equal(prebidResponse[i].requestId); + expect(resp.cpm).to.equal(prebidResponse[i].cpm); + expect(resp.width).to.equal(prebidResponse[i].width); + expect(resp.height).to.equal(prebidResponse[i].height); + expect(resp.ttl).to.equal(prebidResponse[i].ttl); + expect(resp.creativeId).to.equal(prebidResponse[i].creativeId); + expect(resp.netRevenue).to.equal(prebidResponse[i].netRevenue); + expect(resp.currency).to.equal(prebidResponse[i].currency); + expect(resp.dealId).to.equal(prebidResponse[i].dealId); + if (resp.mediaType === 'video') { + expect(resp.vastXml.indexOf('valueimpression')).to.be.greaterThan(0); + } + if (resp.mediaType === 'banner') { + expect(resp.ad.indexOf('Valueimpression AD')).to.be.greaterThan(0); + } + }); + }); + }); + + describe('.getUserSyncs', function () { + let bidResponse = [{ + 'body': { + 'pixel': [{ + 'url': 'https://pixel-test', + 'type': 'image' + }] + } + }]; + + it('should return one sync pixel', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, bidResponse)).to.deep.equal([{ + type: 'image', + url: 'https://pixel-test' + }]); + }); + it('should return an empty array when sync is enabled but there are no bidResponses', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, [])).to.have.length(0); + }); + + it('should return an empty array when sync is enabled but no sync pixel returned', function () { + const pixel = Object.assign({}, bidResponse); + delete pixel[0].body.pixel; + expect(spec.getUserSyncs({ pixelEnabled: true }, bidResponse)).to.have.length(0); + }); + + it('should return an empty array', function () { + expect(spec.getUserSyncs({ pixelEnabled: false }, bidResponse)).to.have.length(0); + expect(spec.getUserSyncs({ pixelEnabled: true }, [])).to.have.length(0); + }); + }); +}); From 07cf06fd449b7423533f87de6c34158169458032 Mon Sep 17 00:00:00 2001 From: sumit sharma Date: Tue, 24 Mar 2020 17:05:43 +0530 Subject: [PATCH 04/90] appnexus adapter : include hb_source in request to server (#4964) * appnexus adapter : include hb_source in request to server * add unit tests * Revert "add unit tests" This reverts commit 7b6f97ccd7dc11411409604d428279d05e441c22. * Revert "appnexus adapter : include hb_source in request to server" This reverts commit 2a242710c387ec618416226ad18d5d6bac7feffe. * move hb_source to tag level * update if condition for adpod Co-authored-by: sumit sharma Co-authored-by: sumit sharma --- modules/appnexusBidAdapter.js | 5 +++ test/spec/modules/appnexusBidAdapter_spec.js | 41 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 9f107bf54ec..ed61dc9c20c 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -659,6 +659,11 @@ function bidToTag(bid) { const videoMediaType = utils.deepAccess(bid, `mediaTypes.${VIDEO}`); const context = utils.deepAccess(bid, 'mediaTypes.video.context'); + if (videoMediaType && context === 'adpod') { + tag.hb_source = 7; + } else { + tag.hb_source = 1; + } if (bid.mediaType === VIDEO || videoMediaType) { tag.ad_types.push(VIDEO); } diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index a3d019c83d2..55f2b21459e 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -164,6 +164,7 @@ describe('AppNexusAdapter', function () { const payload = JSON.parse(request.data); expect(payload.tags[0].ad_types).to.deep.equal(['video']); + expect(payload.tags[0].hb_source).to.deep.equal(1); }); it('sends bid request to ENDPOINT via POST', function () { @@ -193,6 +194,7 @@ describe('AppNexusAdapter', function () { id: 123, minduration: 100 }); + expect(payload.tags[0].hb_source).to.deep.equal(1); }); it('should add video property when adUnit includes a renderer', function () { @@ -417,6 +419,44 @@ describe('AppNexusAdapter', function () { expect(payload3.tags.length).to.equal(15); }); + it('should contain hb_source value for adpod', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 300, + durationRangeSec: [15, 30], + } + } + } + ); + const request = spec.buildRequests([bidRequest])[0]; + const payload = JSON.parse(request.data); + expect(payload.tags[0].hb_source).to.deep.equal(7); + }); + + it('should contain hb_source value for other media', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'banner', + params: { + sizes: [[300, 250], [300, 600]], + placementId: 13144370 + } + } + ); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + it('adds brand_category_exclusion to request when set', function() { let bidRequest = Object.assign({}, bidRequests[0]); sinon @@ -480,6 +520,7 @@ describe('AppNexusAdapter', function () { saleprice: {required: true}, privacy_supported: true }); + expect(payload.tags[0].hb_source).to.equal(1); }); it('should always populated tags[].sizes with 1,1 for native if otherwise not defined', function () { From fecfc2b4ca7f18fbd524631ed23adef9f3cca728 Mon Sep 17 00:00:00 2001 From: Index Exchange 3 Prebid Team Date: Tue, 24 Mar 2020 14:05:44 -0400 Subject: [PATCH 05/90] Index Exchange: Registering user syncs (#5005) * added user sync for ix * tests for userSync iframe drop [LIBRARY-690] * fix user sync test * fix lint * added user sync info to ix bidder doc Co-authored-by: Zicong Zhou Co-authored-by: Pankaj Yadav --- modules/ixBidAdapter.js | 14 ++++++ modules/ixBidAdapter.md | 17 +++++++ test/spec/modules/ixBidAdapter_spec.js | 66 +++++++++++++++++--------- 3 files changed, 75 insertions(+), 22 deletions(-) diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index fd22a21b94f..98896db9337 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -17,6 +17,7 @@ const NET_REVENUE = true; const PRICE_TO_DOLLAR_FACTOR = { JPY: 1 }; +const USER_SYNC_URL = 'https://js-sec.indexww.com/um/ixmatch.html'; /** * Transform valid bid request config object to banner impression object that will be sent to ad server. @@ -459,6 +460,19 @@ export const spec = { return utils.convertTypes({ 'siteID': 'number' }, params); + }, + + /** + * Determine which user syncs should occur + * @param {object} syncOptions + * @param {array} serverResponses + * @returns {array} User sync pixels + */ + getUserSyncs: function (syncOptions, serverResponses) { + return (syncOptions.iframeEnabled) ? [{ + type: 'iframe', + url: USER_SYNC_URL + }] : []; } }; diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index 4335e496efd..fcbc4ea89e3 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -272,6 +272,23 @@ pbjs.setConfig({ }); ``` +#### User Sync +Add the following code to enable user sync. IX strongly recommends enabling user syncing through iframes. This functionality improves DSP user match rates and increases the IX bid rate and bid price. Be sure to call `pbjs.setConfig()` only once. + +``` +pbjs.setConfig({ + userSync: { + iframeEnabled: true, + filterSettings: { + iframe: { + bidders: ['ix'], + filter: 'include' + } + } + } +}); +``` + ### 2. Include `ixBidAdapter` in your build process When running the build command, include `ixBidAdapter` as a module, as well as `dfpAdServerVideo` if you require video support. diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index fe18947b37a..114a2226770 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -197,6 +197,26 @@ describe('IndexexchangeAdapter', function () { }); }); + describe('getUserSync tests', function () { + it('UserSync test : check type = iframe, check usermatch URL', function () { + const syncOptions = { + 'iframeEnabled': true + } + let userSync = spec.getUserSyncs(syncOptions); + expect(userSync[0].type).to.equal('iframe'); + const USER_SYNC_URL = 'https://js-sec.indexww.com/um/ixmatch.html'; + expect(userSync[0].url).to.equal(USER_SYNC_URL); + }); + + it('When iframeEnabled is false, no userSync should be returned', function () { + const syncOptions = { + 'iframeEnabled': false + } + let userSync = spec.getUserSyncs(syncOptions); + expect(userSync).to.be.an('array').that.is.empty; + }); + }); + describe('isBidRequestValid', function () { it('should return true when required params found for a banner or video ad', function () { expect(spec.isBidRequestValid(DEFAULT_BANNER_VALID_BID[0])).to.equal(true); @@ -337,19 +357,19 @@ describe('IndexexchangeAdapter', function () { let query; let testCopy; - beforeEach(function() { + beforeEach(function () { window.headertag = {}; - window.headertag.getIdentityInfo = function() { + window.headertag.getIdentityInfo = function () { return testCopy; }; request = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION)[0]; query = request.data; }); - afterEach(function() { + afterEach(function () { delete window.headertag; }); - describe('buildRequestSingleRTI', function() { - before(function() { + describe('buildRequestSingleRTI', function () { + before(function () { testCopy = JSON.parse(JSON.stringify(DEFAULT_IDENTITY_RESPONSE)); }); it('payload should have correct format and value (single identity partner)', function () { @@ -368,15 +388,17 @@ describe('IndexexchangeAdapter', function () { }); }); - describe('buildRequestMultipleIds', function() { - before(function() { + describe('buildRequestMultipleIds', function () { + before(function () { testCopy = JSON.parse(JSON.stringify(DEFAULT_IDENTITY_RESPONSE)); - testCopy.IdentityIp.data.uids.push({ - id: '1234567' - }, - { - id: '2019-04-01TF2:34:41' - }); + testCopy.IdentityIp.data.uids.push( + { + id: '1234567' + }, + { + id: '2019-04-01TF2:34:41' + } + ); }); it('payload should have correct format and value (single identity w/ multi ids)', function () { const payload = JSON.parse(query.r); @@ -395,8 +417,8 @@ describe('IndexexchangeAdapter', function () { }); }); - describe('buildRequestMultipleRTI', function() { - before(function() { + describe('buildRequestMultipleRTI', function () { + before(function () { testCopy = JSON.parse(JSON.stringify(DEFAULT_IDENTITY_RESPONSE)); testCopy.JackIp = { responsePending: false, @@ -444,13 +466,13 @@ describe('IndexexchangeAdapter', function () { }); }); - describe('buildRequestNoData', function() { - beforeEach(function() { + describe('buildRequestNoData', function () { + beforeEach(function () { testCopy = JSON.parse(JSON.stringify(DEFAULT_IDENTITY_RESPONSE)); }); it('payload should not have any user eids with an undefined identity data response', function () { - window.headertag.getIdentityInfo = function() { + window.headertag.getIdentityInfo = function () { return undefined; }; request = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION)[0]; @@ -960,7 +982,7 @@ describe('IndexexchangeAdapter', function () { }); }); - describe('bidrequest consent', function() { + describe('bidrequest consent', function () { it('should have consent info if gdprApplies and consentString exist', function () { const validBidWithConsent = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION); const requestWithConsent = JSON.parse(validBidWithConsent[0].data.r); @@ -1006,7 +1028,7 @@ describe('IndexexchangeAdapter', function () { expect(requestWithConsent.user).to.be.undefined; }); - it('should have us_privacy if uspConsent is defined', function() { + it('should have us_privacy if uspConsent is defined', function () { const options = { uspConsent: '1YYN' }; @@ -1016,7 +1038,7 @@ describe('IndexexchangeAdapter', function () { expect(requestWithUspConsent.regs.ext.us_privacy).to.equal('1YYN'); }); - it('should not have us_privacy if uspConsent undefined', function() { + it('should not have us_privacy if uspConsent undefined', function () { const options = {}; const validBidWithUspConsent = spec.buildRequests(DEFAULT_BANNER_VALID_BID, options); const requestWithUspConsent = JSON.parse(validBidWithUspConsent[0].data.r); @@ -1024,7 +1046,7 @@ describe('IndexexchangeAdapter', function () { expect(requestWithUspConsent.regs).to.be.undefined; }); - it('should have both gdpr and us_privacy if both are defined', function() { + it('should have both gdpr and us_privacy if both are defined', function () { const options = { gdprConsent: { gdprApplies: true, From f0bc3582aabb89452e95b5ace3f36d32bd93bc30 Mon Sep 17 00:00:00 2001 From: "Isaac A. Dettman" Date: Tue, 24 Mar 2020 12:15:06 -0700 Subject: [PATCH 06/90] update consent manager to support a new config prop: defaultGdprScope (#4970) * Add microadBidAdapter * Remove unnecessary encodeURIComponent from microadBidAdapter * Submit Advangelists Prebid Adapter * Submit Advangelists Prebid Adapter 1.1 * Correct procudtion endpoint for prebid * analytics update with wrapper name * reverted error merge * update changed default value of netRevenue to true * Re-add rubicon analytics without deprecated getTopWindowUrl util * Cache referrer on auction_init instead of bid_requested * add gdprScope config support * add tests * updated with requested changes from the pull request * optimization to simplify setting the gdprApplies value Co-authored-by: nakamoto Co-authored-by: Chandra Prakash Co-authored-by: Eric Harper Co-authored-by: TJ Eastmond Co-authored-by: Mark Monday Co-authored-by: msm0504 <51493331+msm0504@users.noreply.github.com> --- modules/consentManagement.js | 11 ++++--- test/spec/modules/consentManagement_spec.js | 36 +++++++++++++++++++-- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index f27bab70192..39ebea01500 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -17,6 +17,7 @@ const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true; export let userCMP; export let consentTimeout; export let allowAuction; +export let gdprScope; export let staticConsentData; let cmpVersion = 0; @@ -365,17 +366,16 @@ function storeConsentData(cmpConsentObject) { consentData = { consentString: (cmpConsentObject) ? cmpConsentObject.getConsentData.consentData : undefined, vendorData: (cmpConsentObject) ? cmpConsentObject.getVendorConsents : undefined, - gdprApplies: (cmpConsentObject) ? cmpConsentObject.getConsentData.gdprApplies : undefined + gdprApplies: (cmpConsentObject) ? cmpConsentObject.getConsentData.gdprApplies : gdprScope }; } else { consentData = { consentString: (cmpConsentObject) ? cmpConsentObject.tcString : undefined, vendorData: (cmpConsentObject) || undefined, - gdprApplies: (cmpConsentObject) ? cmpConsentObject.gdprApplies : undefined + gdprApplies: (cmpConsentObject) ? cmpConsentObject.gdprApplies : gdprScope }; } consentData.apiVersion = cmpVersion; - gdprDataHandler.setConsentData(consentData); } @@ -434,7 +434,7 @@ export function resetConsentData() { /** * A configuration function that initializes some module variables, as well as add a hook into the requestBids function - * @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) + * @param {{cmp:string, timeout:number, allowAuctionWithoutConsent:boolean, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ export function setConsentConfig(config) { // if `config.gdpr` or `config.usp` exist, assume new config format. @@ -465,6 +465,9 @@ export function setConsentConfig(config) { utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent. Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); } + // if true, then gdprApplies should be set to true + gdprScope = config.defaultGdprScope === true; + utils.logInfo('consentManagement module has been activated...'); if (userCMP === 'static') { diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index fd6973a29d8..fb33094e151 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -1,4 +1,4 @@ -import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, allowAuction, staticConsentData } from 'modules/consentManagement.js'; +import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, allowAuction, staticConsentData, gdprScope } from 'modules/consentManagement.js'; import { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; @@ -25,6 +25,7 @@ describe('consentManagement', function () { expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(10000); expect(allowAuction).to.be.true; + expect(gdprScope).to.be.equal(false); sinon.assert.callCount(utils.logInfo, 4); }); @@ -50,13 +51,15 @@ describe('consentManagement', function () { let allConfig = { cmpApi: 'iab', timeout: 7500, - allowAuctionWithoutConsent: false + allowAuctionWithoutConsent: false, + defaultGdprScope: true }; setConsentConfig(allConfig); expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(7500); expect(allowAuction).to.be.false; + expect(gdprScope).to.be.true; }); it('should use new consent manager config structure for gdpr', function () { @@ -108,6 +111,7 @@ describe('consentManagement', function () { expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(3333); expect(allowAuction).to.be.equal(false); + expect(gdprScope).to.be.equal(false); }); }); @@ -241,6 +245,7 @@ describe('consentManagement', function () { expect(userCMP).to.be.equal('static'); expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used expect(allowAuction).to.be.false; + expect(gdprScope).to.be.equal(false); expect(staticConsentData).to.be.equal(staticConfig.consentData); }); }); @@ -353,6 +358,31 @@ describe('consentManagement', function () { expect(consent.gdprApplies).to.be.true; sinon.assert.notCalled(cmpStub); }); + + it('should not set consent.gdprApplies to true if defaultGdprScope is true', function () { + let testConsentData = { + gdprApplies: false, + consentData: 'xyz' + }; + + cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + args[2](testConsentData); + }); + + setConsentConfig({ + cmpApi: 'iab', + timeout: 7500, + defaultGdprScope: true + }); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + + let consent = gdprDataHandler.getConsentData(); + + expect(consent.gdprApplies).to.be.false; + }); }); describe('iframe tests', function () { @@ -642,7 +672,7 @@ describe('consentManagement', function () { sinon.assert.calledOnce(utils.logWarn); expect(didHookReturn).to.be.true; expect(consent.consentString).to.be.undefined; - expect(consent.gdprApplies).to.be.undefined; + expect(consent.gdprApplies).to.be.false; expect(consent.apiVersion).to.equal(2); }); }); From 498b771308a5c9bc01b10cda65e9e680a795def7 Mon Sep 17 00:00:00 2001 From: xwang202 <57196235+xwang202@users.noreply.github.com> Date: Wed, 25 Mar 2020 03:59:57 +0800 Subject: [PATCH 07/90] freewheel add property vastXml to bidResponse (#5015) --- modules/freewheel-sspBidAdapter.js | 5 +++++ test/spec/modules/freewheel-sspBidAdapter_spec.js | 2 ++ 2 files changed, 7 insertions(+) diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index 69668aa288b..679a7696d0e 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -363,6 +363,11 @@ export const spec = { netRevenue: true, ttl: 360 }; + + if (bidrequest.mediaTypes.video) { + bidResponse.vastXml = serverResponse; + } + bidResponse.ad = formatAdHTML(bidrequest, playerSize); bidResponses.push(bidResponse); } diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index 69e42c1b2e6..a29931d58ff 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -454,6 +454,7 @@ describe('freewheelSSP BidAdapter Test', () => { currency: 'EUR', netRevenue: true, ttl: 360, + vastXml: response, ad: ad } ]; @@ -475,6 +476,7 @@ describe('freewheelSSP BidAdapter Test', () => { currency: 'EUR', netRevenue: true, ttl: 360, + vastXml: response, ad: formattedAd } ]; From da6bb016fd628c725f6f644fdb5c7464fe487639 Mon Sep 17 00:00:00 2001 From: Mikael Lundin Date: Tue, 24 Mar 2020 21:25:56 +0100 Subject: [PATCH 08/90] Added Adnuntius Bid Adapter (#4896) * Added Adnuntius Bid Adapter * Changes to Adnuntius bidder according to comments. * Fixed non used variable warning. * Fixed outstanding issues * Fixed issue in test and bidder. * Fixed all the "for ... of" Issues. --- modules/adnuntiusBidAdapter.js | 68 ++++++++++ modules/adnuntiusBidAdapter.md | 36 ++++++ test/spec/modules/adnuntiusBidAdapter_spec.js | 119 ++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 modules/adnuntiusBidAdapter.js create mode 100644 modules/adnuntiusBidAdapter.md create mode 100644 test/spec/modules/adnuntiusBidAdapter_spec.js diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js new file mode 100644 index 00000000000..16594b3453c --- /dev/null +++ b/modules/adnuntiusBidAdapter.js @@ -0,0 +1,68 @@ + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +const BIDDER_CODE = 'adnuntius'; +const ENDPOINT_URL = 'https://delivery.adnuntius.com/i?tzo=-60&format=json'; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function (bid) { + return !!(bid.params.auId || (bid.params.member && bid.params.invCode)); + }, + + buildRequests: function (validBidRequests) { + const networks = {}; + const bidRequests = {}; + const requests = []; + + for (var i = 0; i < validBidRequests.length; i++) { + const bid = validBidRequests[i] + const network = bid.params.network || 'network'; + bidRequests[network] = bidRequests[network] || []; + bidRequests[network].push(bid); + + networks[network] = networks[network] || {}; + networks[network].adUnits = networks[network].adUnits || []; + networks[network].adUnits.push({ ...bid.params.targeting, auId: bid.params.auId }); + } + + const networkKeys = Object.keys(networks) + for (var j = 0; j < networkKeys.length; j++) { + const network = networkKeys[j]; + requests.push({ + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(networks[network]), + bid: bidRequests[network] + }); + } + + return requests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + const serverBody = serverResponse.body; + + for (var k = 0; k < serverBody.adUnits.length; k++) { + const adUnit = serverBody.adUnits[k] + if (adUnit.matchedAdCount > 0) { + const bid = adUnit.ads[0]; + bidResponses.push({ + requestId: bidRequest.bid[k].bidId, + cpm: (bid.cpm) ? bid.cpm.amount : 0, + width: Number(bid.creativeWidth), + height: Number(bid.creativeHeight), + creativeId: bid.creativeId, + currency: (bid.cpm) ? bid.cpm.currency : 'EUR', + netRevenue: false, + ttl: 360, + ad: adUnit.html + }); + } + } + return bidResponses; + }, + +} +registerBidder(spec); diff --git a/modules/adnuntiusBidAdapter.md b/modules/adnuntiusBidAdapter.md new file mode 100644 index 00000000000..aed12079856 --- /dev/null +++ b/modules/adnuntiusBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: Adnuntius Bidder Adapter +Module Type: Bidder Adapter +Maintainer: info@adnuntius.com +``` + +# Description + +Adnuntius Bidder Adapter for Prebid.js. +Only Banner format is supported. + +# Test Parameters +``` + var adUnits = [ + { + code: "test-div", + mediaTypes: { + banner: { + sizes: [[980, 360], [980, 300], [980, 240], [980, 120]] + } + }, + bids: [ + { + bidder: "adnuntius", + params: { + auId: "8b6bc", + network: "adnuntius", + } + }, + ] + }, + + ]; +``` diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js new file mode 100644 index 00000000000..54ff038c083 --- /dev/null +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -0,0 +1,119 @@ +// import or require modules necessary for the test, e.g.: +import { expect } from 'chai'; // may prefer 'assert' in place of 'expect' +import { spec } from 'modules/adnuntiusBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('adnuntiusBidAdapter', function () { + const ENDPOINT_URL = 'https://delivery.adnuntius.com/i?tzo=-60&format=json'; + const adapter = newBidder(spec); + const bidRequests = [ + { + bidder: 'adnuntius', + params: { + auId: '8b6bc', + network: 'adnuntius', + }, + bidId: '123' + } + ]; + + const serverResponse = { + body: { + 'adUnits': [ + { + 'auId': '000000000008b6bc', + 'targetId': '', + 'html': '

hi!

', + 'matchedAdCount': 1, + 'responseId': 'adn-rsp-1460129238', + 'ads': [ + { + 'destinationUrlEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fc%2F52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN%3Fct%3D2501%26r%3Dhttp%253A%252F%252Fgoogle.com', + 'assets': { + 'image': { + 'cdnId': 'https://assets.adnuntius.com/oEmZa5uYjxENfA1R692FVn6qIveFpO8wUbpyF2xSOCc.jpg', + 'width': '980', + 'height': '120' + } + }, + 'clickUrl': 'https://delivery.adnuntius.com/c/52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'urls': { + 'destination': 'https://delivery.adnuntius.com/c/52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN?ct=2501&r=http%3A%2F%2Fgoogle.com' + }, + 'urlsEsc': { + 'destination': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fc%2F52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN%3Fct%3D2501%26r%3Dhttp%253A%252F%252Fgoogle.com' + }, + 'destinationUrls': { + 'destination': 'http://google.com' + }, + 'cpm': { 'amount': 5.0, 'currency': 'NOK' }, + 'bid': { 'amount': 0.005, 'currency': 'NOK' }, + 'cost': { 'amount': 0.005, 'currency': 'NOK' }, + 'impressionTrackingUrls': [], + 'impressionTrackingUrlsEsc': [], + 'adId': 'adn-id-1347343135', + 'selectedColumn': '0', + 'selectedColumnPosition': '0', + 'renderedPixel': 'https://delivery.adnuntius.com/b/52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN.html', + 'renderedPixelEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fb%2F52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN.html', + 'visibleUrl': 'https://delivery.adnuntius.com/s?rt=52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'visibleUrlEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fs%3Frt%3D52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'viewUrl': 'https://delivery.adnuntius.com/v?rt=52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'viewUrlEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fv%3Frt%3D52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'rt': '52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'creativeWidth': '980', + 'creativeHeight': '120', + 'creativeId': 'wgkq587vgtpchsx1', + 'lineItemId': 'scyjdyv3mzgdsnpf', + 'layoutId': 'sw6gtws2rdj1kwby', + 'layoutName': 'Responsive image' + } + ] + } + ] + } + } + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bidRequests[0])).to.equal(true); + }); + }); + + describe('buildRequests', function () { + it('Test requests', function () { + const request = spec.buildRequests(bidRequests); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('bid'); + const bid = request[0].bid[0] + expect(bid).to.have.property('bidId'); + expect(request[0]).to.have.property('url'); + expect(request[0].url).to.equal(ENDPOINT_URL); + expect(request[0]).to.have.property('data'); + expect(request[0].data).to.equal('{\"adUnits\":[{\"auId\":\"8b6bc\"}]}'); + }); + }); + + describe('interpretResponse', function () { + it('should return valid response when passed valid server response', function () { + const request = spec.buildRequests(bidRequests); + const interpretedResponse = spec.interpretResponse(serverResponse, request[0]); + const ad = serverResponse.body.adUnits[0].ads[0] + expect(interpretedResponse).to.have.lengthOf(1); + expect(interpretedResponse[0].cpm).to.equal(ad.cpm.amount); + expect(interpretedResponse[0].width).to.equal(Number(ad.creativeWidth)); + expect(interpretedResponse[0].height).to.equal(Number(ad.creativeHeight)); + expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); + expect(interpretedResponse[0].currency).to.equal(ad.cpm.currency); + expect(interpretedResponse[0].netRevenue).to.equal(false); + expect(interpretedResponse[0].ad).to.equal(serverResponse.body.adUnits[0].html); + expect(interpretedResponse[0].ttl).to.equal(360); + }); + }); +}); From b24156ceecfb7066ca83c97dbc4f74e6cac23368 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal <7393273+jaiminpanchal27@users.noreply.github.com> Date: Tue, 24 Mar 2020 16:31:44 -0400 Subject: [PATCH 09/90] deal tier fix (#5028) Co-authored-by: Jaimin Panchal --- modules/appnexusBidAdapter.js | 2 +- test/spec/modules/appnexusBidAdapter_spec.js | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index ed61dc9c20c..fc7ec77c267 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -502,7 +502,7 @@ function newBid(serverBid, rtbBid, bidderRequest) { case ADPOD: const iabSubCatId = getIabSubCategory(bidRequest.bidder, rtbBid.brand_category_id); bid.meta = Object.assign({}, bid.meta, { iabSubCatId }); - const dealTier = rtbBid.rtb.dealPriority; + const dealTier = rtbBid.deal_priority; bid.video = { context: ADPOD, durationSeconds: Math.floor(rtbBid.rtb.video.duration_ms / 1000), diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 55f2b21459e..c61b1cf1979 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -1069,17 +1069,29 @@ describe('AppNexusAdapter', function () { it('should add deal_priority and deal_code', function() { let responseWithDeal = deepClone(response); - responseWithDeal.tags[0].ads[0].deal_priority = 'high'; + responseWithDeal.tags[0].ads[0].ad_type = 'video'; + responseWithDeal.tags[0].ads[0].deal_priority = 5; responseWithDeal.tags[0].ads[0].deal_code = '123'; + responseWithDeal.tags[0].ads[0].rtb.video = { + duration_ms: 1500, + player_width: 640, + player_height: 340, + }; let bidderRequest = { bids: [{ bidId: '3db3773286ee59', - adUnitCode: 'code' + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'adpod' + } + } }] } let result = spec.interpretResponse({ body: responseWithDeal }, {bidderRequest}); expect(Object.keys(result[0].appnexus)).to.include.members(['buyerMemberId', 'dealPriority', 'dealCode']); + expect(result[0].video.dealTier).to.equal(5); }); it('should add advertiser id', function() { From a1083ffb2f2604ebac024476fd32add17e03782e Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Wed, 25 Mar 2020 06:08:31 -0700 Subject: [PATCH 10/90] PubMatic bid adapter to support NetId User Module (#5009) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * PubMatic to support NetId UserId sub-module --- modules/pubmaticBidAdapter.js | 1 + test/spec/modules/pubmaticBidAdapter_spec.js | 36 ++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 1a5de695c3c..02c43e1c2a9 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -671,6 +671,7 @@ function _handleEids(payload, validBidRequests) { _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.lipb.lipbid`), 'liveintent.com', 1); _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.parrableid`), 'parrable.com', 1); _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.britepoolid`), 'britepool.com', 1); + _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.netId`), 'netid.de', 1); } if (eids.length > 0) { payload.user.eids = eids; diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 5c2f1f76b2e..75c0beed995 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -1856,6 +1856,42 @@ describe('PubMatic adapter', function () { expect(data.user.eids).to.equal(undefined); }); }); + + describe('NetId', function() { + it('send the NetId if it is present', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.netId = 'netid-user-id'; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.deep.equal([{ + 'source': 'netid.de', + 'uids': [{ + 'id': 'netid-user-id', + 'atype': 1 + }] + }]); + }); + + it('do not pass if not string', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.netId = 1; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.netId = []; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.netId = null; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.netId = {}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + }); + }); }); it('Request params check for video ad', function () { From adda4e126a72b11cd7f1cf4ad2bf1bfc1ccbf963 Mon Sep 17 00:00:00 2001 From: Anna Tudoran Date: Wed, 25 Mar 2020 18:56:04 +0200 Subject: [PATCH 11/90] Undertone add parameters to request (#4995) * * Update undertone adapter - change parameters - placementId parameter is now optional and not mandatory - undertoneBidAdapter.js * Updated undertone bid adapter tests accordingly - undertoneBidAdapter_spec.js * * Update undertone adapter - change parameters - placementId parameter is now optional and not mandatory - undertoneBidAdapter.js * Updated undertone bid adapter tests accordingly - undertoneBidAdapter_spec.js * fix lint issue in undertone adapter spec * added user sync function to undertone adapter * * Update undertone adapter - change parameters - placementId parameter is now optional and not mandatory - undertoneBidAdapter.js * Updated undertone bid adapter tests accordingly - undertoneBidAdapter_spec.js * added user sync function to undertone adapter * added user sync function to undertone adapter * revert package-lock.json * send element coordinates and viewport * Update undertoneBidAdapter.js * add null in case of no data easier integration with earlier versions * update according to fixes * added user sync function to undertone adapter * Update undertoneBidAdapter.js * remove unneded changes after rebase Co-authored-by: omerko Co-authored-by: Omer Koren --- modules/undertoneBidAdapter.js | 27 ++++++- test/spec/modules/undertoneBidAdapter_spec.js | 73 ++++++++++++++++--- 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/modules/undertoneBidAdapter.js b/modules/undertoneBidAdapter.js index a253c6aae5f..923e54183ab 100644 --- a/modules/undertoneBidAdapter.js +++ b/modules/undertoneBidAdapter.js @@ -51,6 +51,26 @@ function getGdprQueryParams(gdprConsent) { return `gdpr=${gdpr}&gdprstr=${gdprstr}`; } +function getBannerCoords(id) { + let element = document.getElementById(id); + let left = -1; + let top = -1; + if (element) { + left = element.offsetLeft; + top = element.offsetTop; + + let parent = element.offsetParent; + if (parent) { + left += parent.offsetLeft; + top += parent.offsetTop; + } + + return [left, top]; + } else { + return null; + } +} + export const spec = { code: BIDDER_CODE, isBidRequestValid: function(bid) { @@ -60,11 +80,15 @@ export const spec = { } }, buildRequests: function(validBidRequests, bidderRequest) { + const vw = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); + const vh = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); + const pageSizeArray = vw == 0 || vh == 0 ? null : [vw, vh]; const payload = { 'x-ut-hb-params': [], 'commons': { 'adapterVersion': '$prebid.version$', - 'uids': validBidRequests[0].userId + 'uids': validBidRequests[0].userId, + 'pageSize': pageSizeArray } }; const referer = bidderRequest.refererInfo.referer; @@ -87,6 +111,7 @@ export const spec = { validBidRequests.map(bidReq => { const bid = { bidRequestId: bidReq.bidId, + coordinates: getBannerCoords(bidReq.adUnitCode), hbadaptor: 'prebid', url: pageUrl, domain: domain, diff --git a/test/spec/modules/undertoneBidAdapter_spec.js b/test/spec/modules/undertoneBidAdapter_spec.js index f379081f88d..e4218019e0d 100644 --- a/test/spec/modules/undertoneBidAdapter_spec.js +++ b/test/spec/modules/undertoneBidAdapter_spec.js @@ -25,6 +25,7 @@ const invalidBidReq = { }; const bidReq = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', bidder: BIDDER_CODE, params: { placementId: '10433394', @@ -35,6 +36,7 @@ const bidReq = [{ auctionId: '9ad1fa8d-2297-4660-a018-b39945054746' }, { + adUnitCode: 'div-gpt-ad-1460505748561-0', bidder: BIDDER_CODE, params: { publisherId: 12345 @@ -54,6 +56,7 @@ const bidReqUserIds = [{ bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746', userId: { + idl_env: '1111', tdid: '123456', digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}} } @@ -145,16 +148,43 @@ const bidResArray = [ } ]; -describe('Undertone Adapter', function () { - describe('request', function () { - it('should validate bid request', function () { +let element; +let sandbox; + +let elementParent = { + offsetLeft: 100, + offsetTop: 100, + offsetHeight: 100, + getAttribute: function() {} +}; + +describe('Undertone Adapter', () => { + describe('request', () => { + it('should validate bid request', () => { expect(spec.isBidRequestValid(validBidReq)).to.equal(true); }); - it('should not validate incorrect bid request', function () { + it('should not validate incorrect bid request', () => { expect(spec.isBidRequestValid(invalidBidReq)).to.equal(undefined); }); }); describe('build request', function () { + beforeEach(function() { + element = { + id: 'div-gpt-ad-1460505748561-0', + offsetLeft: 100, + offsetTop: 100, + offsetWidth: 300, + offsetHeight: 250 + }; + + sandbox = sinon.sandbox.create(); + sandbox.stub(document, 'getElementById').withArgs('div-gpt-ad-1460505748561-0').returns(element); + }); + + afterEach(function() { + sandbox.restore(); + }); + it('should send request to correct url via POST not in GDPR or CCPA', function () { const request = spec.buildRequests(bidReq, bidderReq); const domainStart = bidderReq.refererInfo.referer.indexOf('//'); @@ -202,6 +232,7 @@ describe('Undertone Adapter', function () { expect(bid1.sizes.length).to.equal(2); expect(bid1.placementId).to.equal('10433394'); expect(bid1.publisherId).to.equal(12345); + expect(bid1.coordinates).to.be.an('array'); expect(bid1.params).to.be.an('object'); const bid2 = JSON.parse(request.data)['x-ut-hb-params'][1]; expect(bid2.bidRequestId).to.equal('453cf42d72bb3c'); @@ -216,17 +247,41 @@ describe('Undertone Adapter', function () { expect(bidCommons).to.be.an('object'); expect(bidCommons.uids).to.be.an('object'); expect(bidCommons.uids.tdid).to.equal('123456'); + expect(bidCommons.uids.idl_env).to.equal('1111'); expect(bidCommons.uids.digitrustid.data.id).to.equal('DTID'); }); + it('should send page sizes sizes correctly', function () { + const request = spec.buildRequests(bidReqUserIds, bidderReq); + const bidCommons = JSON.parse(request.data)['commons']; + expect(bidCommons).to.be.an('object'); + expect(bidCommons.pageSize).to.be.an('array'); + expect(bidCommons.pageSize[0]).to.equal(window.innerWidth); + expect(bidCommons.pageSize[1]).to.equal(window.innerHeight); + }); + it('should send banner coordinates', function() { + const request = spec.buildRequests(bidReq, bidderReq); + const bid1 = JSON.parse(request.data)['x-ut-hb-params'][0]; + expect(bid1.coordinates).to.be.an('array'); + expect(bid1.coordinates[0]).to.equal(100); + expect(bid1.coordinates[1]).to.equal(100); + }); + it('should send banner coordinates plus parent', function() { + element.offsetParent = elementParent; + const request = spec.buildRequests(bidReq, bidderReq); + const bid1 = JSON.parse(request.data)['x-ut-hb-params'][0]; + expect(bid1.coordinates).to.be.an('array'); + expect(bid1.coordinates[0]).to.equal(200); + expect(bid1.coordinates[1]).to.equal(200); + }); }); - describe('interpretResponse', function () { - it('should build bid array', function () { + describe('interpretResponse', () => { + it('should build bid array', () => { let result = spec.interpretResponse({body: bidResponse}); expect(result.length).to.equal(1); }); - it('should have all relevant fields', function () { + it('should have all relevant fields', () => { const result = spec.interpretResponse({body: bidResponse}); const bid = result[0]; @@ -240,12 +295,12 @@ describe('Undertone Adapter', function () { expect(bid.ttl).to.equal(360); }); - it('should return empty array when response is incorrect', function () { + it('should return empty array when response is incorrect', () => { expect(spec.interpretResponse({body: {}}).length).to.equal(0); expect(spec.interpretResponse({body: []}).length).to.equal(0); }); - it('should only use valid bid responses', function () { + it('should only use valid bid responses', () => { expect(spec.interpretResponse({ body: bidResArray }).length).to.equal(1); }); }); From e4ea90f7d6470159452008eaf710a952021be20c Mon Sep 17 00:00:00 2001 From: Anand Venkatraman Date: Wed, 25 Mar 2020 13:50:49 -0400 Subject: [PATCH 12/90] PulsePoint bid adapter: Multi-size support (#4988) * ET-1691: Pulsepoint Analytics adapter for Prebid. (#1) * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: cleanup * ET-1691: minor * ET-1691: revert package.json change * Adding bidRequest to bidFactory.createBid method as per https://github.com/prebid/Prebid.js/issues/509 * ET-1765: Adding support for additional params in PulsePoint adapter (#2) * ET-1850: Fixing https://github.com/prebid/Prebid.js/issues/866 * Minor fix * Adding mandatory parameters to Bid * PulsePoint: support for banner.format object * AdSize on response for banner responses * Fixing review comment * Reverting package-json changes --- modules/pulsepointBidAdapter.js | 26 +++++++++-- .../spec/modules/pulsepointBidAdapter_spec.js | 46 +++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index cbe37e66cd7..d3655906cf0 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -133,8 +133,8 @@ function bidResponseAvailable(request, response) { bid.height = idToBidMap[id].h; } else { bid.ad = idToBidMap[id].adm; - bid.width = idToImpMap[id].banner.w; - bid.height = idToImpMap[id].banner.h; + bid.width = idToBidMap[id].w || idToImpMap[id].banner.w; + bid.height = idToBidMap[id].h || idToImpMap[id].banner.h; } bids.push(bid); } @@ -161,14 +161,30 @@ function impression(slot) { * Produces an OpenRTB Banner object for the slot given. */ function banner(slot) { - const size = adSize(slot); + const sizes = parseSizes(slot); + const size = adSize(slot, sizes); return (slot.nativeParams || slot.params.video) ? null : { w: size[0], h: size[1], battr: slot.params.battr, + format: sizes }; } +/** + * Produce openrtb format objects based on the sizes configured for the slot. + */ +function parseSizes(slot) { + const sizes = utils.deepAccess(slot, 'mediaTypes.banner.sizes'); + if (sizes && utils.isArray(sizes)) { + return sizes.filter(sz => utils.isArray(sz) && sz.length === 2).map(sz => ({ + w: sz[0], + h: sz[1] + })); + } + return null; +} + /** * Produces an OpenRTB Video object for the slot given */ @@ -372,12 +388,14 @@ function parse(rawResponse) { /** * Determines the AdSize for the slot. */ -function adSize(slot) { +function adSize(slot, sizes) { if (slot.params.cf) { const size = slot.params.cf.toUpperCase().split('X'); const width = parseInt(slot.params.cw || size[0], 10); const height = parseInt(slot.params.ch || size[1], 10); return [width, height]; + } else if (sizes && sizes.length > 0) { + return [sizes[0].w, sizes[0].h]; } return [1, 1]; } diff --git a/test/spec/modules/pulsepointBidAdapter_spec.js b/test/spec/modules/pulsepointBidAdapter_spec.js index ea81aa5d962..4b21856b68e 100644 --- a/test/spec/modules/pulsepointBidAdapter_spec.js +++ b/test/spec/modules/pulsepointBidAdapter_spec.js @@ -6,6 +6,11 @@ import {deepClone} from 'src/utils.js'; describe('PulsePoint Adapter Tests', function () { const slotConfigs = [{ placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[728, 90], [160, 600]] + } + }, bidId: 'bid12345', params: { cp: 'p10000', @@ -655,4 +660,45 @@ describe('PulsePoint Adapter Tests', function () { userVerify(ortbRequest.user.ext.eids[4], 'parrable.com', 'parrable_id234'); userVerify(ortbRequest.user.ext.eids[5], 'liveintent.com', 'liveintent_id123'); }); + it('Verify multiple adsizes', function () { + const bidRequests = deepClone(slotConfigs); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request).to.be.not.null; + expect(request.data).to.be.not.null; + const ortbRequest = request.data; + expect(ortbRequest.imp).to.have.lengthOf(2); + // first impression has multi sizes + expect(ortbRequest.imp[0].banner).to.not.be.null; + expect(ortbRequest.imp[0].banner.w).to.equal(300); + expect(ortbRequest.imp[0].banner.h).to.equal(250); + expect(ortbRequest.imp[0].banner.format).to.not.be.null; + expect(ortbRequest.imp[0].banner.format).to.have.lengthOf(2); + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(728); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(90); + expect(ortbRequest.imp[0].banner.format[1].w).to.equal(160); + expect(ortbRequest.imp[0].banner.format[1].h).to.equal(600); + // slot 2 + expect(ortbRequest.imp[1].banner).to.not.be.null; + expect(ortbRequest.imp[1].banner.w).to.equal(728); + expect(ortbRequest.imp[1].banner.h).to.equal(90); + expect(ortbRequest.imp[1].banner.format).to.be.null; + // adsize on response + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + crid: 'Creative#123', + w: 728, + h: 90 + }] + }] + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.width).to.equal(728); + expect(bid.height).to.equal(90); + }); }); From 18b40096762cf2fb527550c361354f97a4ffcdcf Mon Sep 17 00:00:00 2001 From: Michael Kuryshev Date: Wed, 25 Mar 2020 20:44:47 +0100 Subject: [PATCH 13/90] VIS.X: use schain & userId modules (#5004) --- modules/visxBidAdapter.js | 26 +++++++++++++- test/spec/modules/visxBidAdapter_spec.js | 44 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index 75011ddad03..e76b6035cd3 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -36,6 +36,8 @@ export const spec = { config.getConfig('currency.adServerCurrency') || DEFAULT_CUR; let reqId; + let payloadSchain; + let payloadUserId; if (currencyWhiteList.indexOf(currency) === -1) { utils.logError(LOG_ERROR_MESS.notAllowedCurrency + currency); @@ -44,8 +46,14 @@ export const spec = { bids.forEach(bid => { reqId = bid.bidderRequestId; - const {params: {uid}, adUnitCode} = bid; + const {params: {uid}, adUnitCode, schain, userId} = bid; auids.push(uid); + if (!payloadSchain && schain) { + payloadSchain = schain; + } + if (!payloadUserId && userId) { + payloadUserId = userId; + } const sizesId = utils.parseSizesInput(bid.sizes); if (!slotsMapByUid[uid]) { @@ -84,6 +92,22 @@ export const spec = { wrapperVersion: '$prebid.version$' }; + if (payloadSchain) { + payload.schain = JSON.stringify(payloadSchain); + } + + if (payloadUserId) { + if (payloadUserId.tdid) { + payload.tdid = payloadUserId.tdid; + } + if (payloadUserId.id5id) { + payload.id5 = payloadUserId.id5id; + } + if (payloadUserId.digitrustid && payloadUserId.digitrustid.data && payloadUserId.digitrustid.data.id) { + payload.dtid = payloadUserId.digitrustid.data.id; + } + } + if (bidderRequest) { if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { payload.u = bidderRequest.refererInfo.referer; diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index 85a66efe795..7720bb2a3e4 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -46,6 +46,14 @@ describe('VisxAdapter', function () { } }; const referrer = bidderRequest.refererInfo.referer; + const schainObject = { + ver: '1.0', + nodes: [ + {asi: 'exchange2.com', sid: 'abcd', hp: 1}, + {asi: 'exchange1.com', sid: '1234!abcd', hp: 1, name: 'publisher, Inc.', domain: 'publisher.com'} + ] + }; + const schainString = JSON.stringify(schainObject); let bidRequests = [ { 'bidder': 'visx', @@ -200,6 +208,42 @@ describe('VisxAdapter', function () { expect(payload).to.have.property('gdpr_consent', 'AAA'); expect(payload).to.have.property('gdpr_applies', 1); }); + + it('if schain is present payload must have schain param', function () { + const schainBidRequests = [ + Object.assign({schain: schainObject}, bidRequests[0]), + bidRequests[1], + bidRequests[2] + ]; + const request = spec.buildRequests(schainBidRequests, bidderRequest); + const payload = request.data; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('schain', schainString); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '903535,903535,903536'); + expect(payload).to.have.property('sizes', '300x250,300x600,728x90'); + expect(payload).to.have.property('r', '22edbae2733bf6'); + expect(payload).to.have.property('cur', 'EUR'); + }); + + it('if userId is available payload must have appropriate params', function () { + const schainBidRequests = [ + Object.assign({userId: { + tdid: '111', + id5id: '222', + digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}} + }}, bidRequests[0]), + bidRequests[1], + bidRequests[2] + ]; + const request = spec.buildRequests(schainBidRequests, bidderRequest); + const payload = request.data; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('tdid', '111'); + expect(payload).to.have.property('id5', '222'); + expect(payload).to.have.property('dtid', 'DTID'); + }); }); describe('interpretResponse', function () { From 811dd4410277b3b953a3300dc59a114f80159c0b Mon Sep 17 00:00:00 2001 From: adfinity-prebid <58843903+adfinity-prebid@users.noreply.github.com> Date: Wed, 25 Mar 2020 23:16:41 +0200 Subject: [PATCH 14/90] ADFINITY adapter gdpr support (#4955) * New adfinity adapter * Update adfinityBidAdapter.md * add test params * native response validattion * fix secure param * gdpr support * fix merge conflict Co-authored-by: Aiholkin Co-authored-by: root --- modules/adfinityBidAdapter.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/adfinityBidAdapter.js b/modules/adfinityBidAdapter.js index 34407e015db..ebf1198fba2 100644 --- a/modules/adfinityBidAdapter.js +++ b/modules/adfinityBidAdapter.js @@ -64,6 +64,13 @@ export const spec = { 'placements': placements }; + if (bidderRequest) { + if (bidderRequest.gdprConsent) { + request.gdpr_consent = bidderRequest.gdprConsent.consentString || 'ALL' + request.gdpr_require = bidderRequest.gdprConsent.gdprApplies ? 1 : 0 + } + } + for (let i = 0; i < validBidRequests.length; i++) { let bid = validBidRequests[i]; let traff = bid.params.traffic || BANNER From 9268e98afe9bc64ce31f525d4e41351eff9def3c Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Wed, 25 Mar 2020 17:36:29 -0400 Subject: [PATCH 15/90] Prebid 3.13.0 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb8d864727e..5dfd6f012c8 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.13.0-pre", + "version": "3.13.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From efa289eb3e19c0a5f0c49034eeec9d9f43af357f Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Wed, 25 Mar 2020 17:50:04 -0400 Subject: [PATCH 16/90] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5dfd6f012c8..a05d8f367d1 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.13.0", + "version": "3.14.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 7c456268913024c77b72aa0d0b0230b0d509aba4 Mon Sep 17 00:00:00 2001 From: Vedant Seta Date: Thu, 26 Mar 2020 10:24:18 +0530 Subject: [PATCH 17/90] Media.net : adds Analytics Adapter (#5016) * adds Media.net Analytics Adapter * fixes medianet adapter key in docs Co-authored-by: vedant.s --- modules/medianetAnalyticsAdapter.js | 622 ++++++++++++++++++ modules/medianetAnalyticsAdapter.md | 23 + .../modules/medianetAnalyticsAdapter_spec.js | 185 ++++++ 3 files changed, 830 insertions(+) create mode 100644 modules/medianetAnalyticsAdapter.js create mode 100644 modules/medianetAnalyticsAdapter.md create mode 100644 test/spec/modules/medianetAnalyticsAdapter_spec.js diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js new file mode 100644 index 00000000000..9cf09209965 --- /dev/null +++ b/modules/medianetAnalyticsAdapter.js @@ -0,0 +1,622 @@ +import adapter from '../src/AnalyticsAdapter'; +import adapterManager from '../src/adapterManager'; +import CONSTANTS from '../src/constants.json'; +import * as utils from '../src/utils'; +import { ajax } from '../src/ajax'; +import { getRefererInfo } from '../src/refererDetection'; +import * as url from '../src/url'; +import { getPriceGranularity, AUCTION_IN_PROGRESS, AUCTION_COMPLETED } from '../src/auction' + +const analyticsType = 'endpoint'; +const ENDPOINT = 'https://pb-logs.media.net/log?logid=kfk&evtid=prebid_analytics_events_client'; +const CONFIG_URL = 'https://prebid.media.net/rtb/prebid/analytics/config'; +const EVENT_PIXEL_URL = 'https://qsearch-a.akamaihd.net/log'; +const DEFAULT_LOGGING_PERCENT = 50; +const PRICE_GRANULARITY = { + 'auto': 'pbAg', + 'custom': 'pbCg', + 'dense': 'pbDg', + 'low': 'pbLg', + 'medium': 'pbMg', + 'high': 'pbHg', +}; + +const MEDIANET_BIDDER_CODE = 'medianet'; +// eslint-disable-next-line no-undef +const PREBID_VERSION = $$PREBID_GLOBAL$$.version; +const ERROR_CONFIG_JSON_PARSE = 'analytics_config_parse_fail'; +const ERROR_CONFIG_FETCH = 'analytics_config_ajax_fail'; +const BID_SUCCESS = 1; +const BID_NOBID = 2; +const BID_TIMEOUT = 3; +const DUMMY_BIDDER = '-2'; + +const CONFIG_PENDING = 0; +const CONFIG_PASS = 1; +const CONFIG_ERROR = 3; + +const VALID_URL_KEY = ['canonical_url', 'og_url', 'twitter_url']; +const DEFAULT_URL_KEY = 'page'; + +let auctions = {}; +let config; +let pageDetails; +let logsQueue = []; + +class ErrorLogger { + constructor(event, additionalData) { + this.event = event; + this.logid = 'kfk'; + this.evtid = 'projectevents'; + this.project = 'prebidanalytics'; + this.dn = pageDetails.domain || ''; + this.requrl = pageDetails.requrl || ''; + this.event = this.event; + this.pbversion = PREBID_VERSION; + this.cid = config.cid || ''; + this.rd = additionalData; + } + + send() { + let url = EVENT_PIXEL_URL + '?' + formatQS(this); + utils.triggerPixel(url); + } +} + +class Configure { + constructor(cid) { + this.cid = cid; + this.pubLper = -1; + this.ajaxState = CONFIG_PENDING; + this.loggingPercent = DEFAULT_LOGGING_PERCENT; + this.urlToConsume = DEFAULT_URL_KEY; + this.debug = false; + this.gdprConsent = undefined; + this.uspConsent = undefined; + } + + set publisherLper(plper) { + this.pubLper = plper; + } + + getLoggingData() { + return { + cid: this.cid, + lper: Math.round(100 / this.loggingPercent), + plper: this.pubLper, + gdpr: this.gdprConsent, + ccpa: this.uspConsent, + ajx: this.ajaxState, + pbv: PREBID_VERSION, + flt: 1, + } + } + + _configURL() { + return CONFIG_URL + '?cid=' + encodeURIComponent(this.cid) + '&dn=' + encodeURIComponent(pageDetails.domain); + } + + _parseResponse(response) { + try { + response = JSON.parse(response); + if (isNaN(response.percentage)) { + throw new Error('not a number'); + } + this.loggingPercent = response.percentage; + this.urlToConsume = VALID_URL_KEY.includes(response.urlKey) ? response.urlKey : this.urlToConsume; + this.ajaxState = CONFIG_PASS; + } catch (e) { + this.ajaxState = CONFIG_ERROR; + /* eslint no-new: "error" */ + new ErrorLogger(ERROR_CONFIG_JSON_PARSE, e).send(); + } + } + + _errorFetch() { + this.ajaxState = CONFIG_ERROR; + /* eslint no-new: "error" */ + new ErrorLogger(ERROR_CONFIG_FETCH).send(); + } + + init() { + // Forces Logging % to 100% + let urlObj = url.parse(pageDetails.page); + if (utils.deepAccess(urlObj, 'search.medianet_test') || urlObj.hostname === 'localhost') { + this.loggingPercent = 100; + this.ajaxState = CONFIG_PASS; + this.debug = true; + return; + } + ajax( + this._configURL(), + { + success: this._parseResponse.bind(this), + error: this._errorFetch.bind(this) + } + ); + } +} + +class PageDetail { + constructor () { + const canonicalUrl = this._getUrlFromSelector('link[rel="canonical"]', 'href'); + const ogUrl = this._getUrlFromSelector('meta[property="og:url"]', 'content'); + const twitterUrl = this._getUrlFromSelector('meta[name="twitter:url"]', 'content'); + const refererInfo = getRefererInfo(); + + this.domain = url.parse(refererInfo.referer).host; + this.page = refererInfo.referer; + this.is_top = refererInfo.reachedTop; + this.referrer = this._getTopWindowReferrer(); + this.canonical_url = canonicalUrl; + this.og_url = ogUrl; + this.twitter_url = twitterUrl; + this.screen = this._getWindowSize() + } + + _getTopWindowReferrer() { + try { + return window.top.document.referrer; + } catch (e) { + return document.referrer; + } + } + + _getWindowSize() { + let w = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth || -1; + let h = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || -1; + return `${w}x${h}`; + } + + _getAttributeFromSelector(selector, attribute) { + try { + let doc = utils.getWindowTop().document; + let element = doc.querySelector(selector); + if (element !== null && element[attribute]) { + return element[attribute]; + } + } catch (e) {} + } + + _getAbsoluteUrl(url) { + let aTag = utils.getWindowTop().document.createElement('a'); + aTag.href = url; + + return aTag.href; + } + + _getUrlFromSelector(selector, attribute) { + let attr = this._getAttributeFromSelector(selector, attribute); + return attr && this._getAbsoluteUrl(attr); + } + + getLoggingData() { + return { + requrl: this[config.urlToConsume] || this.page, + dn: this.domain, + ref: this.referrer, + screen: this.screen + } + } +} + +class AdSlot { + constructor(mediaTypes, bannerSizes, tmax, supplyAdCode, adext) { + this.mediaTypes = mediaTypes; + this.bannerSizes = bannerSizes; + this.tmax = tmax; + this.supplyAdCode = supplyAdCode; + this.adext = adext; + this.logged = false; + this.targeting = undefined; + } + + getLoggingData() { + return Object.assign({ + supcrid: this.supplyAdCode, + mediaTypes: this.mediaTypes && this.mediaTypes.join('|'), + szs: this.bannerSizes.join('|'), + tmax: this.tmax, + targ: JSON.stringify(this.targeting) + }, + this.adext && {'adext': JSON.stringify(this.adext)}, + ); + } +} + +class Bid { + constructor(bidId, bidder, src, start, supplyAdCode) { + this.bidId = bidId; + this.bidder = bidder; + this.src = src; + this.start = start; + this.supplyAdCode = supplyAdCode; + this.iwb = 0; + this.winner = 0; + this.status = bidder === DUMMY_BIDDER ? BID_SUCCESS : BID_TIMEOUT; + this.ext = {}; + this.originalCpm = undefined; + this.cpm = undefined; + this.dfpbd = undefined; + this.width = undefined; + this.height = undefined; + this.mediaType = undefined; + this.timeToRespond = undefined; + this.dealId = undefined; + this.creativeId = undefined; + this.adId = undefined; + this.currency = undefined; + this.crid = undefined; + this.pubcrid = undefined; + this.mpvid = undefined; + } + + get size() { + if (!this.width || !this.height) { + return ''; + } + return this.width + 'x' + this.height; + } + + getLoggingData() { + return { + pvnm: this.bidder, + src: this.src, + ogbdp: this.originalCpm, + bdp: this.cpm, + cbdp: this.dfpbd, + dfpbd: this.dfpbd, + size: this.size, + mtype: this.mediaType, + dId: this.dealId, + winner: this.winner, + curr: this.currency, + rests: this.timeToRespond, + status: this.status, + iwb: this.iwb, + crid: this.crid, + pubcrid: this.pubcrid, + mpvid: this.mpvid, + ext: JSON.stringify(this.ext) + } + } +} + +class Auction { + constructor(acid) { + this.acid = acid; + this.status = AUCTION_IN_PROGRESS; + this.bids = []; + this.adSlots = {}; + this.auctionInitTime = undefined; + this.auctionStartTime = undefined; + this.setTargetingTime = undefined; + this.auctionEndTime = undefined; + this.bidWonTime = undefined; + } + + hasEnded() { + return this.status === AUCTION_COMPLETED; + } + + getLoggingData() { + return { + sts: this.auctionStartTime - this.auctionInitTime, + ets: this.auctionEndTime - this.auctionInitTime, + tts: this.setTargetingTime - this.auctionInitTime, + wts: this.bidWonTime - this.auctionInitTime, + aucstatus: this.status + } + } + + addSlot(supplyAdCode, { mediaTypes, bannerSizes, tmax, adext }) { + if (supplyAdCode && this.adSlots[supplyAdCode] === undefined) { + this.adSlots[supplyAdCode] = new AdSlot(mediaTypes, bannerSizes, tmax, supplyAdCode, adext); + this.addBid( + new Bid('-1', DUMMY_BIDDER, 'client', '-1', supplyAdCode) + ); + } + } + + addBid(bid) { + this.bids.push(bid); + } + + findBid(key, value) { + return this.bids.filter(bid => { + return bid[key] === value + })[0]; + } + + getAdslotBids(adslot) { + return this.bids + .filter((bid) => bid.supplyAdCode === adslot) + .map((bid) => bid.getLoggingData()); + } + + getWinnerAdslotBid(adslot) { + return this.getAdslotBids(adslot).filter((bid) => bid.winner); + } +} + +function auctionInitHandler({auctionId, timestamp}) { + if (auctionId && auctions[auctionId] === undefined) { + auctions[auctionId] = new Auction(auctionId); + auctions[auctionId].auctionInitTime = timestamp; + } +} + +function bidRequestedHandler({ auctionId, auctionStart, bids, start, timeout, uspConsent, gdprConsent }) { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + config.gdprConsent = config.gdprConsent || gdprConsent; + config.uspConsent = config.uspConsent || uspConsent; + + bids.forEach(bid => { + const { adUnitCode, bidder, mediaTypes, sizes, bidId, src } = bid; + if (!auctions[auctionId].adSlots[adUnitCode]) { + auctions[auctionId].auctionStartTime = auctionStart; + auctions[auctionId].addSlot( + adUnitCode, + Object.assign({}, + (mediaTypes instanceof Object) && { mediaTypes: Object.keys(mediaTypes) }, + { bannerSizes: utils.deepAccess(mediaTypes, 'banner.sizes') || sizes || [] }, + { adext: utils.deepAccess(mediaTypes, 'banner.ext') || '' }, + { tmax: timeout } + ) + ); + } + let bidObj = new Bid(bidId, bidder, src, start, adUnitCode); + auctions[auctionId].addBid(bidObj); + if (bidder === MEDIANET_BIDDER_CODE) { + bidObj.crid = utils.deepAccess(bid, 'params.crid'); + bidObj.pubcrid = utils.deepAccess(bid, 'params.crid'); + } + }); +} + +function bidResponseHandler(bid) { + const { width, height, mediaType, cpm, requestId, timeToRespond, auctionId, dealId } = bid; + const {originalCpm, bidderCode, creativeId, adId, currency} = bid; + + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', requestId); + if (!(bidObj instanceof Bid)) { + return; + } + Object.assign( + bidObj, + { cpm, width, height, mediaType, timeToRespond, dealId, creativeId }, + { adId, currency, originalCpm } + ); + let dfpbd = utils.deepAccess(bid, 'adserverTargeting.hb_pb'); + if (!dfpbd) { + let priceGranularity = getPriceGranularity(mediaType, bid); + let priceGranularityKey = PRICE_GRANULARITY[priceGranularity]; + dfpbd = bid[priceGranularityKey] || cpm; + } + bidObj.dfpbd = dfpbd; + bidObj.status = BID_SUCCESS; + + if (bidderCode === MEDIANET_BIDDER_CODE && bid.ext instanceof Object) { + Object.assign( + bidObj, + { 'ext': bid.ext }, + { 'mpvid': bid.ext.pvid }, + bid.ext.crid && { 'crid': bid.ext.crid } + ); + } +} + +function noBidResponseHandler({ auctionId, bidId }) { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + if (auctions[auctionId].hasEnded()) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', bidId); + if (!(bidObj instanceof Bid)) { + return; + } + bidObj.status = BID_NOBID; +} + +function bidTimeoutHandler(timedOutBids) { + timedOutBids.map(({bidId, auctionId}) => { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', bidId); + if (!(bidObj instanceof Bid)) { + return; + } + bidObj.status = BID_TIMEOUT; + }) +} + +function auctionEndHandler({ auctionId, auctionEnd }) { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + auctions[auctionId].status = AUCTION_COMPLETED; + auctions[auctionId].auctionEndTime = auctionEnd; +} + +function setTargetingHandler(params) { + for (const adunit of Object.keys(params)) { + for (const auctionId of Object.keys(auctions)) { + let auctionObj = auctions[auctionId]; + let adunitObj = auctionObj.adSlots[adunit]; + if (!(adunitObj instanceof AdSlot)) { + continue; + } + adunitObj.targeting = params[adunit]; + auctionObj.setTargetingTime = Date.now(); + let targetingObj = Object.keys(params[adunit]).reduce((result, key) => { + if (key.indexOf('hb_adid') !== -1) { + result[key] = params[adunit][key] + } + return result; + }, {}); + let bidAdIds = Object.keys(targetingObj).map(k => targetingObj[k]); + auctionObj.bids.filter((bid) => bidAdIds.indexOf(bid.adId) !== -1).map(function(bid) { + bid.iwb = 1; + }); + sendEvent(auctionId, adunit, false); + } + } +} + +function bidWonHandler(bid) { + const { requestId, auctionId, adUnitCode } = bid; + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', requestId); + if (!(bidObj instanceof Bid)) { + return; + } + auctions[auctionId].bidWonTime = Date.now(); + bidObj.winner = 1; + sendEvent(auctionId, adUnitCode, true); +} + +function isSampled() { + return Math.random() * 100 < parseFloat(config.loggingPercent); +} + +function isValidAuctionAdSlot(acid, adtag) { + return (auctions[acid] instanceof Auction) && (auctions[acid].adSlots[adtag] instanceof AdSlot); +} + +function sendEvent(id, adunit, isBidWonEvent) { + if (!isValidAuctionAdSlot(id, adunit)) { + return; + } + if (isBidWonEvent) { + fireAuctionLog(id, adunit, isBidWonEvent); + } else if (isSampled() && !auctions[id].adSlots[adunit].logged) { + auctions[id].adSlots[adunit].logged = true; + fireAuctionLog(id, adunit, isBidWonEvent); + } +} + +function getCommonLoggingData(acid, adtag) { + let commonParams = Object.assign(pageDetails.getLoggingData(), config.getLoggingData()); + let adunitParams = auctions[acid].adSlots[adtag].getLoggingData(); + let auctionParams = auctions[acid].getLoggingData(); + return Object.assign(commonParams, adunitParams, auctionParams); +} + +function fireAuctionLog(acid, adtag, isBidWonEvent) { + let commonParams = getCommonLoggingData(acid, adtag); + let targeting = utils.deepAccess(commonParams, 'targ'); + + Object.keys(commonParams).forEach((key) => (commonParams[key] == null) && delete commonParams[key]); + delete commonParams.targ; + + let bidParams; + + if (isBidWonEvent) { + bidParams = auctions[acid].getWinnerAdslotBid(adtag); + commonParams.lper = 1; + } else { + bidParams = auctions[acid].getAdslotBids(adtag).map(({winner, ...restParams}) => restParams); + delete commonParams.wts; + } + let mnetPresent = bidParams.filter(b => b.pvnm === MEDIANET_BIDDER_CODE).length > 0; + if (!mnetPresent) { + bidParams = bidParams.map(({mpvid, crid, ext, pubcrid, ...restParams}) => restParams); + } + + let url = formatQS(commonParams) + '&'; + bidParams.forEach(function(bidParams) { + url = url + formatQS(bidParams) + '&'; + }); + url = url + formatQS({targ: targeting}); + firePixel(url); +} + +function formatQS(data) { + return utils._map(data, (value, key) => value === undefined ? key + '=' : key + '=' + encodeURIComponent(value)).join('&'); +} + +function firePixel(qs) { + logsQueue.push(ENDPOINT + '&' + qs); + utils.triggerPixel(ENDPOINT + '&' + qs); +} + +let medianetAnalytics = Object.assign(adapter({URL, analyticsType}), { + getlogsQueue() { + return logsQueue; + }, + clearlogsQueue() { + logsQueue = []; + auctions = {}; + }, + track({ eventType, args }) { + if (config.debug) { + utils.logInfo(eventType, args); + } + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: { + auctionInitHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_REQUESTED: { + bidRequestedHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_RESPONSE: { + bidResponseHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_TIMEOUT: { + bidTimeoutHandler(args); + break; + } + case CONSTANTS.EVENTS.NO_BID: { + noBidResponseHandler(args); + break; + } + case CONSTANTS.EVENTS.AUCTION_END: { + auctionEndHandler(args); + break; + } + case CONSTANTS.EVENTS.SET_TARGETING : { + setTargetingHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_WON: { + bidWonHandler(args); + break; + } + } + }}); + +medianetAnalytics.originEnableAnalytics = medianetAnalytics.enableAnalytics; + +medianetAnalytics.enableAnalytics = function (configuration) { + if (!configuration || !configuration.options || !configuration.options.cid) { + utils.logError('Media.net Analytics adapter: cid is required.'); + return; + } + pageDetails = new PageDetail(); + + config = new Configure(configuration.options.cid); + config.publisherLper = configuration.options.sampling || ''; + config.init(); + configuration.options.sampling = 1; + medianetAnalytics.originEnableAnalytics(configuration); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: medianetAnalytics, + code: 'medianetAnalytics' +}); + +export default medianetAnalytics; diff --git a/modules/medianetAnalyticsAdapter.md b/modules/medianetAnalyticsAdapter.md new file mode 100644 index 00000000000..60ab6c0dd6b --- /dev/null +++ b/modules/medianetAnalyticsAdapter.md @@ -0,0 +1,23 @@ +# Overview +Module Name: media.net Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: prebid-support@media.net + +# Description + +Analytics adapter for Media.net +https://media.net/ + +# Test Parameters + +``` +{ + provider: 'medianetAnalytics', + options : { + cid: '8CUX0H51C' + } +} + +``` \ No newline at end of file diff --git a/test/spec/modules/medianetAnalyticsAdapter_spec.js b/test/spec/modules/medianetAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..dcec1050652 --- /dev/null +++ b/test/spec/modules/medianetAnalyticsAdapter_spec.js @@ -0,0 +1,185 @@ +import { expect } from 'chai'; +import medianetAnalytics from 'modules/medianetAnalyticsAdapter.js'; +import * as utils from 'src/utils.js'; +import CONSTANTS from 'src/constants.json'; +import events from 'src/events.js'; + +const { + EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, NO_BID, BID_TIMEOUT, AUCTION_END, SET_TARGETING, BID_WON } +} = CONSTANTS; + +const MOCK = { + AUCTION_INIT: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739}, + BID_REQUESTED: {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, + BID_RESPONSE: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, + AUCTION_END: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'auctionEnd': 1584563605739}, + SET_TARGETING: {'div-gpt-ad-1460505748561-0': {'prebid_test': '1', 'hb_format': 'banner', 'hb_source': 'client', 'hb_size': '300x250', 'hb_pb': '2.00', 'hb_adid': '3e6e4bce5c8fb3', 'hb_bidder': 'medianet', 'hb_format_medianet': 'banner', 'hb_source_medianet': 'client', 'hb_size_medianet': '300x250', 'hb_pb_medianet': '2.00', 'hb_adid_medianet': '3e6e4bce5c8fb3', 'hb_bidder_medianet': 'medianet'}}, + BID_WON: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'statusMessage': 'Bid available', 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, + NO_BID: {'bidder': 'medianet', 'params': {'cid': 'test123', 'crid': '451466393', 'site': {}}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '303fa0c6-682f-4aea-8e4a-dc68f0d5c7d5', 'sizes': [[300, 250], [300, 600]], 'bidId': '28248b0e6aece2', 'bidderRequestId': '13fccf3809fe43', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}, + BID_TIMEOUT: [{'bidId': '28248b0e6aece2', 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'params': [{'cid': 'test123', 'crid': '451466393', 'site': {}}, {'cid': '8CUX0H51P', 'crid': '451466393', 'site': {}}], 'timeout': 6}] +} + +function performStandardAuctionWithWinner() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON); +} + +function performStandardAuctionWithNoBid() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(NO_BID, MOCK.NO_BID); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); +} + +function performStandardAuctionWithTimeout() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); +} + +function getQueryData(url) { + const queryArgs = url.split('?')[1].split('&'); + return queryArgs.reduce((data, arg) => { + const [key, val] = arg.split('='); + if (data[key] !== undefined) { + if (!Array.isArray(data[key])) { + data[key] = [data[key]]; + } + data[key].push(val); + } else { + data[key] = val; + } + return data; + }, {}); +} + +describe('Media.net Analytics Adapter', function() { + let sandbox; + let CUSTOMER_ID = 'test123'; + let VALID_CONFIGURATION = { + options: { + cid: CUSTOMER_ID + } + } + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('Configuration', function() { + it('should log error if publisher id is not passed', function() { + sandbox.stub(utils, 'logError'); + + medianetAnalytics.enableAnalytics(); + expect( + utils.logError.calledWith( + 'Media.net Analytics adapter: cid is required.' + ) + ).to.be.true; + }); + + it('should not log error if valid config is passed', function() { + sandbox.stub(utils, 'logError'); + + medianetAnalytics.enableAnalytics(VALID_CONFIGURATION); + expect(utils.logError.called).to.equal(false); + medianetAnalytics.disableAnalytics(); + }); + }); + + describe('Events', function() { + beforeEach(function () { + medianetAnalytics.enableAnalytics({ + options: { + cid: 'test123' + } + }); + }); + afterEach(function () { + medianetAnalytics.disableAnalytics(); + }); + + it('should not log if only Auction Init', function() { + medianetAnalytics.clearlogsQueue(); + medianetAnalytics.track({ AUCTION_INIT }) + expect(medianetAnalytics.getlogsQueue().length).to.equal(0); + }); + + it('should have winner log in standard auction', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithWinner(); + let winnerLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter((log) => log.winner); + medianetAnalytics.clearlogsQueue(); + + expect(winnerLog.length).to.equal(1); + }); + + it('should have correct values in winner log', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithWinner(); + let winnerLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter((log) => log.winner); + medianetAnalytics.clearlogsQueue(); + + expect(winnerLog[0]).to.include({ + winner: '1', + pvnm: 'medianet', + curr: 'USD', + src: 'client', + size: '300x250', + mtype: 'banner', + cid: 'test123', + lper: '1', + ogbdp: '1.1495', + flt: '1', + supcrid: 'div-gpt-ad-1460505748561-0', + mpvid: '123' + }); + }); + + it('should have no bid status', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithNoBid(); + let noBidLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); + noBidLog = noBidLog[0]; + + medianetAnalytics.clearlogsQueue(); + expect(noBidLog.pvnm).to.have.ordered.members(['-2', 'medianet']); + expect(noBidLog.iwb).to.have.ordered.members(['0', '0']); + expect(noBidLog.status).to.have.ordered.members(['1', '2']); + expect(noBidLog.src).to.have.ordered.members(['client', 'client']); + expect(noBidLog.curr).to.have.ordered.members(['', '']); + expect(noBidLog.mtype).to.have.ordered.members(['', '']); + expect(noBidLog.ogbdp).to.have.ordered.members(['', '']); + expect(noBidLog.mpvid).to.have.ordered.members(['', '']); + expect(noBidLog.crid).to.have.ordered.members(['', '451466393']); + }); + + it('should have timeout status', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithTimeout(); + let timeoutLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); + timeoutLog = timeoutLog[0]; + + medianetAnalytics.clearlogsQueue(); + expect(timeoutLog.pvnm).to.have.ordered.members(['-2', 'medianet']); + expect(timeoutLog.iwb).to.have.ordered.members(['0', '0']); + expect(timeoutLog.status).to.have.ordered.members(['1', '3']); + expect(timeoutLog.src).to.have.ordered.members(['client', 'client']); + expect(timeoutLog.curr).to.have.ordered.members(['', '']); + expect(timeoutLog.mtype).to.have.ordered.members(['', '']); + expect(timeoutLog.ogbdp).to.have.ordered.members(['', '']); + expect(timeoutLog.mpvid).to.have.ordered.members(['', '']); + expect(timeoutLog.crid).to.have.ordered.members(['', '451466393']); + }); + }); +}); From b9676bf541aab9f4fd9a6eac5e3bbb5c0ec912d3 Mon Sep 17 00:00:00 2001 From: OneTagDevOps <38786435+OneTagDevOps@users.noreply.github.com> Date: Thu, 26 Mar 2020 13:00:56 +0100 Subject: [PATCH 18/90] Onetag bid Adapter: added video support. (#4986) * Cambia nome variabile us privacy in getUserSyncs * Onetag bid adapter: Added video support and minor code refactoring * Fix for linter * Onetag Bid Adapter: code refactoring, added testing params and updated adapter tests * Onetag Bid Adapter: minor code refactoring Co-authored-by: valentino Co-authored-by: Nicola --- modules/onetagBidAdapter.js | 200 ++++++++++++++------- modules/onetagBidAdapter.md | 34 ++-- test/spec/modules/onetagBidAdapter_spec.js | 195 +++++++++++++++----- 3 files changed, 315 insertions(+), 114 deletions(-) diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index d6ed19c4af5..f2cea34bb1a 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -1,18 +1,11 @@ 'use strict'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; const { registerBidder } = require('../src/adapters/bidderFactory.js'); const ENDPOINT = 'https://onetag-sys.com/prebid-request'; const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; const BIDDER_CODE = 'onetag'; -const BANNER = 'banner'; - -// ======= -// Object BidRequest -// -// .params -// required .pubId: string -// optional .type: "BANNER" | "VIDEO" | "NATIVE" --> only BANNER at present /** * Determines whether or not the given bid request is valid. @@ -21,15 +14,28 @@ const BANNER = 'banner'; * @return boolean True if this is a valid bid, and false otherwise. */ function isBidRequestValid(bid) { - if (typeof bid === 'undefined' || bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { + if (typeof bid === 'undefined' || typeof bid.params === 'undefined' || typeof bid.params.pubId !== 'string') { return false; } + return isValid(BANNER, bid) || isValid(VIDEO, bid); +} - if (typeof bid.params.pubId !== 'string' || typeof bid.sizes === 'undefined' || bid.sizes.length === 0) { - return false; - } +export function hasTypeVideo(bid) { + return typeof bid.mediaTypes !== 'undefined' && typeof bid.mediaTypes.video !== 'undefined'; +} - return true; +export function isValid(type, bid) { + if (type === BANNER) { + return parseSizes(bid).length > 0; + } else if (type === VIDEO && hasTypeVideo(bid)) { + const context = bid.mediaTypes.video.context; + if (context === 'outstream') { + return parseVideoSize(bid).length > 0 && typeof bid.renderer !== 'undefined' && typeof bid.renderer.render !== 'undefined' && typeof bid.renderer.url !== 'undefined'; + } else if (context === 'instream') { + return parseVideoSize(bid).length > 0; + } + } + return false; } /** @@ -40,7 +46,7 @@ function isBidRequestValid(bid) { */ function buildRequests(validBidRequests, bidderRequest) { - const bids = validBidRequests.map(requestsToBids); + const bids = requestsToBids(validBidRequests); const bidObject = {'bids': bids}; const pageInfo = getPageInfo(); @@ -57,6 +63,10 @@ function buildRequests(validBidRequests, bidderRequest) { payload.usPrivacy = bidderRequest.uspConsent; } + if (bidderRequest && bidderRequest.userId) { + payload.userId = bidderRequest.userId; + } + const payloadString = JSON.stringify(payload); return { @@ -81,25 +91,33 @@ function interpretResponse(serverResponse, request) { if (!body || (body.nobid && body.nobid === true)) { return bids; } - - if (body.bids) { - body.bids.forEach(function(bid) { - bids.push({ - requestId: bid.requestId, - cpm: bid.cpm, - width: bid.width, - height: bid.height, - creativeId: bid.creativeId, - dealId: bid.dealId ? bid.dealId : '', - currency: bid.currency, - netRevenue: false, - mediaType: bids.type ? bids.type : BANNER, - ad: bid.ad, - ttl: bid.ttl || 300 - }); - }); + if (!body.bids || !Array.isArray(body.bids) || body.bids.length === 0) { + return bids; } + body.bids.forEach(function(bid) { + let responseBid = { + requestId: bid.requestId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.creativeId, + dealId: bid.dealId ? bid.dealId : '', + currency: bid.currency, + netRevenue: false, + mediaType: bid.mediaType, + ttl: bid.ttl || 300 + }; + + if (bid.mediaType === BANNER) { + responseBid.ad = bid.ad; + } else if (bid.mediaType === VIDEO) { + responseBid.vastXml = bid.ad; + } + + bids.push(responseBid); + }); + return bids; } @@ -117,9 +135,30 @@ function getPageInfo() { break } } - - const params = { - + let isDocHidden; + let xOffset; + let yOffset; + try { + if (typeof w.document.hidden !== 'undefined') { + isDocHidden = w.document.hidden; + } else if (typeof w.document['msHidden'] !== 'undefined') { + isDocHidden = w.document['msHidden']; + } else if (typeof w.document['webkitHidden'] !== 'undefined') { + isDocHidden = w.document['webkitHidden']; + } else { + isDocHidden = null; + } + } catch (e) { + isDocHidden = null; + } + try { + xOffset = w.pageXOffset; + yOffset = w.pageYOffset; + } catch (e) { + xOffset = null; + yOffset = null; + } + return { location: e(l), referrer: e(r) || '0', masked: m, @@ -133,45 +172,86 @@ function getPageInfo() { aHeight: s.availHeight, sLeft: 'screenLeft' in w ? w.screenLeft : w.screenX, sTop: 'screenTop' in w ? w.screenTop : w.screenY, + xOffset: xOffset, + yOffset: yOffset, + docHidden: isDocHidden, hLength: history.length, date: t.toUTCString(), timeOffset: t.getTimezoneOffset() }; - - return params; } -function requestsToBids(bid) { - const toRet = {}; - - const params = bid.params; +function requestsToBids(bidRequests) { + const videoBidRequests = bidRequests.filter(bidRequest => hasTypeVideo(bidRequest)).map(bidRequest => { + const videoObj = {}; + setGeneralInfo.call(videoObj, bidRequest); + // Pass parameters + // Context: instream - outstream - adpod + videoObj['context'] = bidRequest.mediaTypes.video.context; + // MIME Video Types + videoObj['mimes'] = bidRequest.mediaTypes.video.mimes; + // Sizes + videoObj['playerSize'] = parseVideoSize(bidRequest); + // Other params + videoObj['protocols'] = bidRequest.mediaTypes.video.protocols; + videoObj['maxDuration'] = bidRequest.mediaTypes.video.maxduration; + videoObj['api'] = bidRequest.mediaTypes.video.api; + videoObj['type'] = VIDEO; + return videoObj; + }); + const bannerBidRequests = bidRequests.filter(bidRequest => isValid(BANNER, bidRequest)).map(bidRequest => { + const bannerObj = {}; + setGeneralInfo.call(bannerObj, bidRequest); + bannerObj['sizes'] = parseSizes(bidRequest); + bannerObj['type'] = BANNER; + return bannerObj; + }); + return videoBidRequests.concat(bannerBidRequests); +} - toRet['adUnitCode'] = bid.adUnitCode; - toRet['bidId'] = bid.bidId; - toRet['bidderRequestId'] = bid.bidderRequestId; - toRet['auctionId'] = bid.auctionId; - toRet['transactionId'] = bid.transactionId; - toRet['sizes'] = []; - const sizes = bid.sizes; - for (let i = 0, lenght = sizes.length; i < lenght; i++) { - const size = sizes[i]; - toRet['sizes'].push({width: size[0], height: size[1]}) +function setGeneralInfo(bidRequest) { + const params = bidRequest.params; + this['adUnitCode'] = bidRequest.adUnitCode; + this['bidId'] = bidRequest.bidId; + this['bidderRequestId'] = bidRequest.bidderRequestId; + this['auctionId'] = bidRequest.auctionId; + this['transactionId'] = bidRequest.transactionId; + this['pubId'] = params.pubId; + if (params.pubClick) { + this['click'] = params.pubClick; } - - toRet['pubId'] = params.pubId; - if (params.type) { - toRet['type'] = params.type; + if (params.dealId) { + this['dealId'] = params.dealId; } +} - if (params.pubClick) { - toRet['click'] = params.pubClick; +function parseVideoSize(bid) { + const playerSize = bid.mediaTypes.video.playerSize; + if (typeof playerSize !== 'undefined' && Array.isArray(playerSize) && playerSize.length > 0) { + return getSizes(playerSize) } + return []; +} - if (params.dealId) { - toRet['dealId'] = params.dealId; +function parseSizes(bid) { + let ret = []; + if (typeof bid.mediaTypes !== 'undefined' && typeof bid.mediaTypes.banner !== 'undefined' && typeof bid.mediaTypes.banner.sizes !== 'undefined' && Array.isArray(bid.mediaTypes.banner.sizes) && bid.mediaTypes.banner.sizes.length > 0) { + return getSizes(bid.mediaTypes.banner.sizes) } + const isVideoBidRequest = hasTypeVideo(bid); + if (!isVideoBidRequest && bid.sizes && Array.isArray(bid.sizes)) { + return getSizes(bid.sizes); + } + return ret; +} - return toRet; +function getSizes(sizes) { + const ret = []; + for (let i = 0, lenght = sizes.length; i < lenght; i++) { + const size = sizes[i]; + ret.push({width: size[0], height: size[1]}) + } + return ret; } function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { @@ -202,7 +282,7 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: isBidRequestValid, buildRequests: buildRequests, diff --git a/modules/onetagBidAdapter.md b/modules/onetagBidAdapter.md index 38872ad8280..9757be4d440 100644 --- a/modules/onetagBidAdapter.md +++ b/modules/onetagBidAdapter.md @@ -8,27 +8,39 @@ Maintainer: devops@onetag.com # Description -OneTag Bid Adapter supports only banner at present. +OneTag Bid Adapter supports banner and video at present. # Test Parameters ``` var adUnits = [ { - code: "test-div", + code: "banner-space", mediaTypes: { banner: { sizes: [[300, 250]] } }, - bids: [ - { - bidder: "onetag", - params: { - pubId: "your_publisher_id", // required - type: "banner" // optional. Default "banner" - }, + bids: [{ + bidder: "onetag", + params: { + pubId: "your_publisher_id" // required, testing pubId: "386276e072" } - ] - }]; + }] + }, { + code: 'video-space', + mediaTypes: { + video: { + context: "instream", + mimes: ["video/mp4", "video/webm", "application/javascript", "video/ogg"], + playerSize: [640,480] + } + }, + bids: [{ + bidder: "onetag", + params: { + pubId: "your_publisher_id" // required, testing pubId: "386276e072" + } + }] + }]; ``` diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index 791077d15b9..2b31c875502 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -1,40 +1,117 @@ -import { spec } from 'modules/onetagBidAdapter.js'; +import { spec, isValid, hasTypeVideo } from 'modules/onetagBidAdapter.js'; import { expect } from 'chai'; +import { BANNER, VIDEO } from 'src/mediaTypes.js'; describe('onetag', function () { - let bid = { - 'bidder': 'onetag', - 'params': { - 'pubId': '386276e072', - }, - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'transactionId': 'qwerty123' - }; + function createBid() { + return { + 'bidder': 'onetag', + 'params': { + 'pubId': '386276e072', + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': 'qwerty123' + }; + } + + function createBannerBid(bidRequest) { + const bid = bidRequest || createBid(); + bid.mediaTypes = bid.mediaTypes || {}; + bid.mediaTypes.banner = { + sizes: [[300, 250]] + }; + return bid; + } + + function createVideoBid(bidRequest) { + const bid = bidRequest || createBid(); + bid.mediaTypes = bid.mediaTypes || {}; + bid.mediaTypes.video = { + context: 'instream', + mimes: ['video/mp4', 'video/webm', 'application/javascript', 'video/ogg'], + playerSize: [640, 480] + }; + return bid; + } + + function createWrongVideoOutstreamBid(bidRequest) { + const bid = bidRequest || createBid(); + bid.mediaTypes = bid.mediaTypes || {}; + bid.mediaTypes.video = { + context: 'outstream', + mimes: ['video/mp4', 'video/webm', 'application/javascript', 'video/ogg'], + playerSize: [640, 480] + }; + return bid; + } + + function createMultiFormatBid() { + return createVideoBid(createBannerBid()); + } + + const bannerBid = createBannerBid(); + const videoBid = createVideoBid(); + const outstreamVideoBid = createWrongVideoOutstreamBid(); describe('isBidRequestValid', function () { it('Should return true when required params are found', function () { - expect(spec.isBidRequestValid(bid)).to.be.true; + expect(spec.isBidRequestValid(bannerBid)).to.be.true; }); it('Should return false when pubId is not a string', function () { - bid.params.pubId = 30; - expect(spec.isBidRequestValid(bid)).to.be.false; + bannerBid.params.pubId = 30; + expect(spec.isBidRequestValid(bannerBid)).to.be.false; }); it('Should return false when pubId is undefined', function () { - bid.params.pubId = undefined; - expect(spec.isBidRequestValid(bid)).to.be.false; + bannerBid.params.pubId = undefined; + expect(spec.isBidRequestValid(bannerBid)).to.be.false; }); - it('Should return false when the sizes array is empty', function () { - bid.sizes = []; - expect(spec.isBidRequestValid(bid)).to.be.false; + describe('banner bidRequest', function () { + it('Should return false when the sizes array is empty', function () { + bannerBid.sizes = []; + expect(spec.isBidRequestValid(bannerBid)).to.be.false; + }); + }); + describe('video bidRequest', function () { + it('Should return false when the context is undefined', function () { + videoBid.mediaTypes.video.context = undefined; + expect(spec.isBidRequestValid(videoBid)).to.be.false; + }); + it('Should return false when the context is not instream or outstream', function () { + videoBid.mediaTypes.video.context = 'wrong'; + expect(spec.isBidRequestValid(videoBid)).to.be.false; + }); + it('Should return false when playerSize is undefined', function () { + const videoBid = createVideoBid(); + videoBid.mediaTypes.video.playerSize = undefined; + expect(spec.isBidRequestValid(videoBid)).to.be.false; + }); + it('Should return false when playerSize is not an array', function () { + const videoBid = createVideoBid(); + videoBid.mediaTypes.video.playerSize = 30; + expect(spec.isBidRequestValid(videoBid)).to.be.false; + }); + it('Should return false when playerSize is an empty array', function () { + const videoBid = createVideoBid(); + videoBid.mediaTypes.video.playerSize = []; + expect(spec.isBidRequestValid(videoBid)).to.be.false; + }); + it('Should return false when context is outstream but no renderer object is defined', function () { + expect(spec.isBidRequestValid(outstreamVideoBid)).to.be.false; + }); + }); + describe('multi format bidRequest', function () { + const multiFormatBid = createMultiFormatBid(); + it('Should return true when correct multi format bid is passed', function () { + expect(spec.isBidRequestValid(multiFormatBid)).to.be.true; + }); }); }); describe('buildRequests', function () { - let serverRequest = spec.buildRequests([bid]); + let serverRequest = spec.buildRequests([bannerBid, videoBid]); it('Creates a ServerRequest object with method, URL and data', function () { expect(serverRequest).to.exist; expect(serverRequest.method).to.exist; @@ -53,7 +130,7 @@ describe('onetag', function () { const data = JSON.parse(d); it('Should contains all keys', function () { expect(data).to.be.an('object'); - expect(data).to.have.all.keys('location', 'masked', 'referrer', 'sHeight', 'sWidth', 'timeOffset', 'date', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids'); + expect(data).to.have.all.keys('location', 'masked', 'referrer', 'sHeight', 'sWidth', 'timeOffset', 'date', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset'); expect(data.location).to.be.a('string'); expect(data.masked).to.be.a('number'); expect(data.referrer).to.be.a('string'); @@ -75,7 +152,11 @@ describe('onetag', function () { const bids = data['bids']; for (let i = 0; i < bids.length; i++) { const bid = bids[i]; - expect(bid).to.have.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'pubId', 'transactionId', 'sizes'); + if (hasTypeVideo(bid)) { + expect(bid).to.have.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'pubId', 'transactionId', 'context', 'mimes', 'playerSize', 'protocols', 'maxDuration', 'api', 'type'); + } else if (isValid(BANNER, bid)) { + expect(bid).to.have.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'pubId', 'transactionId', 'sizes', 'type'); + } expect(bid.bidId).to.be.a('string'); expect(bid.pubId).to.be.a('string'); } @@ -101,7 +182,7 @@ describe('onetag', function () { gdprApplies: true } }; - let serverRequest = spec.buildRequests([bid], bidderRequest); + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); const payload = JSON.parse(serverRequest.data); expect(payload).to.exist; @@ -118,7 +199,7 @@ describe('onetag', function () { 'timeout': 3000, 'uspConsent': consentString }; - let serverRequest = spec.buildRequests([bid], bidderRequest); + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); const payload = JSON.parse(serverRequest.data); expect(payload.usPrivacy).to.exist; @@ -126,33 +207,61 @@ describe('onetag', function () { }); }); describe('interpretResponse', function () { - const resObject = { - body: { - nobid: false, - bids: [{ - ad: '
Advertising
', - cpm: 13, - width: 300, - height: 250, - creativeId: '1820', - dealId: 'dishfo', - currency: 'USD', - requestId: 'sdiceobxcw' - }] + function getBannerRes() { + return { + ad: '
Advertising
', + cpm: 13, + width: 300, + height: 250, + creativeId: '1820', + dealId: 'dishfo', + currency: 'USD', + requestId: 'sdiceobxcw', + mediaType: BANNER } - }; + } + function getVideoRes() { + return { + ad: '', + cpm: 13, + width: 300, + height: 250, + creativeId: '1820', + dealId: 'dishfo', + currency: 'USD', + requestId: 'sdiceobxcw', + mediaType: VIDEO + } + } + function getBannerAdnVideoRes() { + return { + body: { + nobid: false, + bids: [getBannerRes(), getVideoRes()] + } + }; + } + const responseObj = getBannerAdnVideoRes(); it('Returns an array of valid server responses if response object is valid', function () { - const serverResponses = spec.interpretResponse(resObject); + const serverResponses = spec.interpretResponse(responseObj); expect(serverResponses).to.be.an('array').that.is.not.empty; for (let i = 0; i < serverResponses.length; i++) { let dataItem = serverResponses[i]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'dealId'); + if (dataItem.mediaType === VIDEO) { + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastXml', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'dealId'); + } else if (dataItem.mediaType === BANNER) { + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'dealId'); + } expect(dataItem.requestId).to.be.a('string'); expect(dataItem.cpm).to.be.a('number'); expect(dataItem.width).to.be.a('number'); expect(dataItem.height).to.be.a('number'); - expect(dataItem.ad).to.be.a('string'); + if (dataItem.mediaType === VIDEO) { + expect(dataItem.vastXml).to.be.a('string'); + } else if (dataItem.mediaType === BANNER) { + expect(dataItem.ad).to.be.a('string'); + } expect(dataItem.ttl).to.be.a('number'); expect(dataItem.creativeId).to.be.a('string'); expect(dataItem.netRevenue).to.be.a('boolean'); From a344de952200b1f5e169b81d078cc76f78d02c61 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen <14229985+Fawke@users.noreply.github.com> Date: Thu, 26 Mar 2020 19:02:25 +0530 Subject: [PATCH 19/90] add file extension to imports (#5034) --- modules/medianetAnalyticsAdapter.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js index 9cf09209965..63f2e2329a2 100644 --- a/modules/medianetAnalyticsAdapter.js +++ b/modules/medianetAnalyticsAdapter.js @@ -1,11 +1,11 @@ -import adapter from '../src/AnalyticsAdapter'; -import adapterManager from '../src/adapterManager'; +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import * as utils from '../src/utils'; -import { ajax } from '../src/ajax'; -import { getRefererInfo } from '../src/refererDetection'; -import * as url from '../src/url'; -import { getPriceGranularity, AUCTION_IN_PROGRESS, AUCTION_COMPLETED } from '../src/auction' +import * as utils from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import * as url from '../src/url.js'; +import { getPriceGranularity, AUCTION_IN_PROGRESS, AUCTION_COMPLETED } from '../src/auction.js' const analyticsType = 'endpoint'; const ENDPOINT = 'https://pb-logs.media.net/log?logid=kfk&evtid=prebid_analytics_events_client'; From be3fafac4504a2165d7cf83f9712485533f4646f Mon Sep 17 00:00:00 2001 From: varashellov Date: Thu, 26 Mar 2020 20:37:11 +0300 Subject: [PATCH 20/90] Update platformioBidAdapter.js 3.0 (#4965) * Add files via upload * Add files via upload * Update platformioBidAdapter.js --- modules/platformioBidAdapter.js | 307 +++++++++++++++ modules/platformioBidAdapter.md | 7 +- .../spec/modules/platformioBidAdapter_spec.js | 348 ++++++++++++++++++ 3 files changed, 657 insertions(+), 5 deletions(-) create mode 100644 modules/platformioBidAdapter.js create mode 100644 test/spec/modules/platformioBidAdapter_spec.js diff --git a/modules/platformioBidAdapter.js b/modules/platformioBidAdapter.js new file mode 100644 index 00000000000..ad25e262d56 --- /dev/null +++ b/modules/platformioBidAdapter.js @@ -0,0 +1,307 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import includes from 'core-js/library/fn/array/includes.js'; + +const NATIVE_DEFAULTS = { + TITLE_LEN: 100, + DESCR_LEN: 200, + SPONSORED_BY_LEN: 50, + IMG_MIN: 150, + ICON_MIN: 50, +}; +const DEFAULT_MIMES = ['video/mp4', 'video/webm', 'application/x-shockwave-flash', 'application/javascript']; +const VIDEO_TARGETING = ['mimes', 'skippable', 'playback_method', 'protocols', 'api']; +const DEFAULT_PROTOCOLS = [2, 3, 5, 6]; +const DEFAULT_APIS = [1, 2]; + +export const spec = { + + code: 'platformio', + supportedMediaTypes: ['banner', 'native', 'video'], + + isBidRequestValid: bid => ( + !!(bid && bid.params && bid.params.pubId && bid.params.placementId) + ), + buildRequests: (bidRequests, bidderRequest) => { + const request = { + id: bidRequests[0].bidderRequestId, + at: 2, + imp: bidRequests.map(slot => impression(slot)), + site: site(bidRequests), + app: app(bidRequests), + device: device(bidRequests), + }; + applyGdpr(bidderRequest, request); + return { + method: 'POST', + url: 'https://piohbdisp.hb.adx1.com/', + data: JSON.stringify(request), + }; + }, + interpretResponse: (response, request) => ( + bidResponseAvailable(request, response.body) + ), +}; + +function bidResponseAvailable(bidRequest, bidResponse) { + const idToImpMap = {}; + const idToBidMap = {}; + const ortbRequest = parse(bidRequest.data); + ortbRequest.imp.forEach(imp => { + idToImpMap[imp.id] = imp; + }); + if (bidResponse) { + bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => { + idToBidMap[bid.impid] = bid; + })); + } + const bids = []; + Object.keys(idToImpMap).forEach(id => { + if (idToBidMap[id]) { + const bid = {}; + bid.requestId = id; + bid.adId = id; + bid.creativeId = id; + bid.cpm = idToBidMap[id].price; + bid.currency = bidResponse.cur; + bid.ttl = 360; + bid.netRevenue = true; + if (idToImpMap[id]['native']) { + bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); + let nurl = idToBidMap[id].nurl; + nurl = nurl.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); + bid['native']['impressionTrackers'] = [nurl]; + bid.mediaType = 'native'; + } else if (idToImpMap[id]['video']) { + bid.vastUrl = idToBidMap[id].adm; + bid.vastUrl = bid.vastUrl.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + bid.crid = idToBidMap[id].crid; + bid.width = idToImpMap[id].video.w; + bid.height = idToImpMap[id].video.h; + bid.mediaType = 'video'; + } else if (idToImpMap[id]['banner']) { + bid.ad = idToBidMap[id].adm; + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_AD_ID(%7D|\})/gi, idToBidMap[id].adid); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); + bid.width = idToBidMap[id].w; + bid.height = idToBidMap[id].h; + bid.mediaType = 'banner'; + } + bids.push(bid); + } + }); + return bids; +} +function impression(slot) { + return { + id: slot.bidId, + secure: window.location.protocol === 'https:' ? 1 : 0, + 'banner': banner(slot), + 'native': nativeImpression(slot), + 'video': videoImpression(slot), + bidfloor: slot.params.bidFloor || '0.000001', + tagid: slot.params.placementId.toString(), + }; +} + +function banner(slot) { + if (slot.mediaType === 'banner' || utils.deepAccess(slot, 'mediaTypes.banner')) { + const sizes = utils.deepAccess(slot, 'mediaTypes.banner.sizes'); + if (sizes.length > 1) { + let format = []; + for (let f = 0; f < sizes.length; f++) { + format.push({'w': sizes[f][0], 'h': sizes[f][1]}); + } + return {'format': format}; + } else { + return { + w: sizes[0][0], + h: sizes[0][1] + } + } + } + return null; +} + +function videoImpression(slot) { + if (slot.mediaType === 'video' || utils.deepAccess(slot, 'mediaTypes.video')) { + const sizes = utils.deepAccess(slot, 'mediaTypes.video.playerSize'); + const video = { + w: sizes[0][0], + h: sizes[0][1], + mimes: DEFAULT_MIMES, + protocols: DEFAULT_PROTOCOLS, + api: DEFAULT_APIS, + }; + if (slot.params.video) { + Object.keys(slot.params.video).filter(param => includes(VIDEO_TARGETING, param)).forEach(param => video[param] = slot.params.video[param]); + } + return video; + } + return null; +} + +function nativeImpression(slot) { + if (slot.mediaType === 'native' || utils.deepAccess(slot, 'mediaTypes.native')) { + const assets = []; + addAsset(assets, titleAsset(1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); + addAsset(assets, dataAsset(2, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); + addAsset(assets, dataAsset(3, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); + addAsset(assets, imageAsset(4, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN)); + addAsset(assets, imageAsset(5, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); + return { + request: JSON.stringify({ assets }), + ver: '1.1', + }; + } + return null; +} + +function addAsset(assets, asset) { + if (asset) { + assets.push(asset); + } +} + +function titleAsset(id, params, defaultLen) { + if (params) { + return { + id, + required: params.required ? 1 : 0, + title: { + len: params.len || defaultLen, + }, + }; + } + return null; +} + +function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) { + return params ? { + id, + required: params.required ? 1 : 0, + img: { + type, + wmin: params.wmin || defaultMinWidth, + hmin: params.hmin || defaultMinHeight, + } + } : null; +} + +function dataAsset(id, params, type, defaultLen) { + return params ? { + id, + required: params.required ? 1 : 0, + data: { + type, + len: params.len || defaultLen, + } + } : null; +} + +function site(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.pubId : '0'; + const siteId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.siteId : '0'; + const appParams = bidderRequest[0].params.app; + if (!appParams) { + return { + publisher: { + id: pubId.toString(), + domain: window.location.hostname, + }, + id: siteId.toString(), + ref: window.top.document.referrer, + page: window.location.href, + } + } + return null; +} + +function app(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.pubId : '0'; + const appParams = bidderRequest[0].params.app; + if (appParams) { + return { + publisher: { + id: pubId.toString(), + }, + id: appParams.id, + name: appParams.name, + bundle: appParams.bundle, + storeurl: appParams.storeUrl, + domain: appParams.domain, + } + } + return null; +} + +function device(bidderRequest) { + const lat = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.latitude : ''; + const lon = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.longitude : ''; + const ifa = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.ifa : ''; + return { + dnt: utils.getDNT() ? 1 : 0, + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + w: (window.screen.width || window.innerWidth), + h: (window.screen.height || window.innerHeigh), + geo: { + lat: lat, + lon: lon, + }, + ifa: ifa, + }; +} + +function parse(rawResponse) { + try { + if (rawResponse) { + return JSON.parse(rawResponse); + } + } catch (ex) { + utils.logError('platformio.parse', 'ERROR', ex); + } + return null; +} + +function applyGdpr(bidderRequest, ortbRequest) { + if (bidderRequest && bidderRequest.gdprConsent) { + ortbRequest.regs = { ext: { gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0 } }; + ortbRequest.user = { ext: { consent: bidderRequest.gdprConsent.consentString } }; + } +} + +function nativeResponse(imp, bid) { + if (imp['native']) { + const nativeAd = parse(bid.adm); + const keys = {}; + keys.image = {}; + keys.icon = {}; + if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) { + nativeAd['native'].assets.forEach(asset => { + keys.title = asset.title ? asset.title.text : keys.title; + keys.body = asset.data && asset.id === 2 ? asset.data.value : keys.body; + keys.sponsoredBy = asset.data && asset.id === 3 ? asset.data.value : keys.sponsoredBy; + keys.icon.url = asset.img && asset.id === 4 ? asset.img.url : keys.icon.url; + keys.icon.width = asset.img && asset.id === 4 ? asset.img.w : keys.icon.width; + keys.icon.height = asset.img && asset.id === 4 ? asset.img.h : keys.icon.height; + keys.image.url = asset.img && asset.id === 5 ? asset.img.url : keys.image.url; + keys.image.width = asset.img && asset.id === 5 ? asset.img.w : keys.image.width; + keys.image.height = asset.img && asset.id === 5 ? asset.img.h : keys.image.height; + }); + if (nativeAd['native'].link) { + keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url); + } + return keys; + } + } + return null; +} + +registerBidder(spec); diff --git a/modules/platformioBidAdapter.md b/modules/platformioBidAdapter.md index ff6335d1d70..863e023f0d7 100644 --- a/modules/platformioBidAdapter.md +++ b/modules/platformioBidAdapter.md @@ -14,7 +14,6 @@ Please use ```platformio``` as the bidder code. ``` var adUnits = [{ code: 'dfp-native-div', - mediaType: 'native', mediaTypes: { native: { title: { @@ -50,7 +49,7 @@ Please use ```platformio``` as the bidder code. mediaTypes: { banner: { sizes: [ - [300, 250] + [300, 250],[300,600] ], } }, @@ -59,16 +58,15 @@ Please use ```platformio``` as the bidder code. params: { pubId: '29521', siteId: '26049', - size: '300X250', placementId: '123', } }] }, { code: 'dfp-video-div', - sizes: [640, 480], mediaTypes: { video: { + playerSize: [[640, 480]], context: "instream" } }, @@ -77,7 +75,6 @@ Please use ```platformio``` as the bidder code. params: { pubId: '29521', siteId: '26049', - size: '640X480', placementId: '123', video: { skipppable: true, diff --git a/test/spec/modules/platformioBidAdapter_spec.js b/test/spec/modules/platformioBidAdapter_spec.js new file mode 100644 index 00000000000..ee753be17a7 --- /dev/null +++ b/test/spec/modules/platformioBidAdapter_spec.js @@ -0,0 +1,348 @@ +import {expect} from 'chai'; +import {spec} from 'modules/platformioBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; + +describe('Platform.io Adapter Tests', function () { + const slotConfigs = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bidId: 'bid12345', + mediaType: 'banner', + params: { + pubId: '29521', + siteId: '26047', + placementId: '123', + bidFloor: '0.001', + ifa: 'IFA', + latitude: '40.712775', + longitude: '-74.005973' + } + }, { + placementCode: '/DfpAccount2/slot2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bidId: 'bid23456', + mediaType: 'banner', + params: { + pubId: '29521', + siteId: '26047', + placementId: '1234', + bidFloor: '0.000001', + } + }]; + const nativeSlotConfig = [{ + placementCode: '/DfpAccount1/slot3', + bidId: 'bid12345', + mediaType: 'native', + nativeParams: { + title: { required: true, len: 200 }, + body: {}, + image: { wmin: 100 }, + sponsoredBy: { }, + icon: { } + }, + params: { + pubId: '29521', + placementId: '123', + siteId: '26047' + } + }]; + const videoSlotConfig = [{ + placementCode: '/DfpAccount1/slot4', + mediaTypes: { + video: { + playerSize: [[640, 480]] + } + }, + bidId: 'bid12345678', + mediaType: 'video', + video: { + skippable: true + }, + params: { + pubId: '29521', + placementId: '1234567', + siteId: '26047', + } + }]; + const appSlotConfig = [{ + placementCode: '/DfpAccount1/slot5', + bidId: 'bid12345', + params: { + pubId: '29521', + placementId: '1234', + app: { + id: '1111', + name: 'app name', + bundle: 'io.platform.apps', + storeUrl: 'https://platform.io/apps', + domain: 'platform.io' + } + } + }]; + + it('Verify build request', function () { + const request = spec.buildRequests(slotConfigs); + expect(request.url).to.equal('https://piohbdisp.hb.adx1.com/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + // site object + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site.publisher).to.not.equal(null); + expect(ortbRequest.site.publisher.id).to.equal('29521'); + expect(ortbRequest.site.ref).to.equal(window.top.document.referrer); + expect(ortbRequest.site.page).to.equal(window.location.href); + expect(ortbRequest.imp).to.have.lengthOf(2); + // device object + expect(ortbRequest.device).to.not.equal(null); + expect(ortbRequest.device.ua).to.equal(navigator.userAgent); + expect(ortbRequest.device.ifa).to.equal('IFA'); + expect(ortbRequest.device.geo.lat).to.equal('40.712775'); + expect(ortbRequest.device.geo.lon).to.equal('-74.005973'); + // slot 1 + expect(ortbRequest.imp[0].tagid).to.equal('123'); + expect(ortbRequest.imp[0].banner).to.not.equal(null); + expect(ortbRequest.imp[0].banner.w).to.equal(300); + expect(ortbRequest.imp[0].banner.h).to.equal(250); + expect(ortbRequest.imp[0].bidfloor).to.equal('0.001'); + // slot 2 + expect(ortbRequest.imp[1].tagid).to.equal('1234'); + expect(ortbRequest.imp[1].banner).to.not.equal(null); + expect(ortbRequest.imp[1].banner.w).to.equal(728); + expect(ortbRequest.imp[1].banner.h).to.equal(90); + expect(ortbRequest.imp[1].bidfloor).to.equal('0.000001'); + }); + + it('Verify parse response', function () { + const request = spec.buildRequests(slotConfigs); + const ortbRequest = JSON.parse(request.data); + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + w: 300, + h: 250 + }] + }], + cur: 'USD' + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.ad).to.equal('This is an Ad'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.adId).to.equal('bid12345'); + expect(bid.creativeId).to.equal('bid12345'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(360); + }); + + it('Verify full passback', function () { + const request = spec.buildRequests(slotConfigs); + const bids = spec.interpretResponse({ body: null }, request) + expect(bids).to.have.lengthOf(0); + }); + + it('Verify Native request', function () { + const request = spec.buildRequests(nativeSlotConfig); + expect(request.url).to.equal('https://piohbdisp.hb.adx1.com/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + // native impression + expect(ortbRequest.imp[0].tagid).to.equal('123'); + const nativePart = ortbRequest.imp[0]['native']; + expect(nativePart).to.not.equal(null); + expect(nativePart.ver).to.equal('1.1'); + expect(nativePart.request).to.not.equal(null); + // native request assets + const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); + expect(nativeRequest).to.not.equal(null); + expect(nativeRequest.assets).to.have.lengthOf(5); + expect(nativeRequest.assets[0].id).to.equal(1); + expect(nativeRequest.assets[1].id).to.equal(2); + expect(nativeRequest.assets[2].id).to.equal(3); + expect(nativeRequest.assets[3].id).to.equal(4); + expect(nativeRequest.assets[4].id).to.equal(5); + expect(nativeRequest.assets[0].required).to.equal(1); + expect(nativeRequest.assets[0].title).to.not.equal(null); + expect(nativeRequest.assets[0].title.len).to.equal(200); + expect(nativeRequest.assets[1].title).to.be.undefined; + expect(nativeRequest.assets[1].data).to.not.equal(null); + expect(nativeRequest.assets[1].data.type).to.equal(2); + expect(nativeRequest.assets[1].data.len).to.equal(200); + expect(nativeRequest.assets[2].required).to.equal(0); + expect(nativeRequest.assets[3].img).to.not.equal(null); + expect(nativeRequest.assets[3].img.wmin).to.equal(50); + expect(nativeRequest.assets[3].img.hmin).to.equal(50); + expect(nativeRequest.assets[3].img.type).to.equal(1); + expect(nativeRequest.assets[4].img).to.not.equal(null); + expect(nativeRequest.assets[4].img.wmin).to.equal(100); + expect(nativeRequest.assets[4].img.hmin).to.equal(150); + expect(nativeRequest.assets[4].img.type).to.equal(3); + }); + + it('Verify Native response', function () { + const request = spec.buildRequests(nativeSlotConfig); + expect(request.url).to.equal('https://piohbdisp.hb.adx1.com/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + const nativeResponse = { + 'native': { + assets: [ + { id: 1, title: { text: 'Ad Title' } }, + { id: 2, data: { value: 'Test description' } }, + { id: 3, data: { value: 'Brand' } }, + { id: 4, img: { url: 'https://adx1public.s3.amazonaws.com/creatives_icon.png', w: 100, h: 100 } }, + { id: 5, img: { url: 'https://adx1public.s3.amazonaws.com/creatives_image.png', w: 300, h: 300 } } + ], + link: { url: 'https://brand.com/' } + } + }; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + nurl: 'https://rtb.adx1.com/log', + adm: JSON.stringify(nativeResponse) + }] + }], + cur: 'USD', + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + // verify bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.adId).to.equal('bid12345'); + expect(bid.ad).to.be.undefined; + expect(bid.mediaType).to.equal('native'); + const nativeBid = bid['native']; + expect(nativeBid).to.not.equal(null); + expect(nativeBid.title).to.equal('Ad Title'); + expect(nativeBid.sponsoredBy).to.equal('Brand'); + expect(nativeBid.icon.url).to.equal('https://adx1public.s3.amazonaws.com/creatives_icon.png'); + expect(nativeBid.image.url).to.equal('https://adx1public.s3.amazonaws.com/creatives_image.png'); + expect(nativeBid.image.width).to.equal(300); + expect(nativeBid.image.height).to.equal(300); + expect(nativeBid.icon.width).to.equal(100); + expect(nativeBid.icon.height).to.equal(100); + expect(nativeBid.clickUrl).to.equal(encodeURIComponent('https://brand.com/')); + expect(nativeBid.impressionTrackers).to.have.lengthOf(1); + expect(nativeBid.impressionTrackers[0]).to.equal('https://rtb.adx1.com/log'); + }); + + it('Verify Video request', function () { + const request = spec.buildRequests(videoSlotConfig); + expect(request.url).to.equal('https://piohbdisp.hb.adx1.com/'); + expect(request.method).to.equal('POST'); + const videoRequest = JSON.parse(request.data); + // site object + expect(videoRequest.site).to.not.equal(null); + expect(videoRequest.site.publisher.id).to.equal('29521'); + expect(videoRequest.site.ref).to.equal(window.top.document.referrer); + expect(videoRequest.site.page).to.equal(window.location.href); + // device object + expect(videoRequest.device).to.not.equal(null); + expect(videoRequest.device.ua).to.equal(navigator.userAgent); + // slot 1 + expect(videoRequest.imp[0].tagid).to.equal('1234567'); + expect(videoRequest.imp[0].video).to.not.equal(null); + expect(videoRequest.imp[0].video.w).to.equal(640); + expect(videoRequest.imp[0].video.h).to.equal(480); + expect(videoRequest.imp[0].banner).to.equal(null); + expect(videoRequest.imp[0].native).to.equal(null); + }); + + it('Verify parse video response', function () { + const request = spec.buildRequests(videoSlotConfig); + const videoRequest = JSON.parse(request.data); + const videoResponse = { + seatbid: [{ + bid: [{ + impid: videoRequest.imp[0].id, + price: 1.90, + adm: 'https://vid.example.com/9876', + crid: '510511_754567308' + }] + }], + cur: 'USD' + }; + const bids = spec.interpretResponse({ body: videoResponse }, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.90); + expect(bid.vastUrl).to.equal('https://vid.example.com/9876'); + expect(bid.crid).to.equal('510511_754567308'); + expect(bid.width).to.equal(640); + expect(bid.height).to.equal(480); + expect(bid.adId).to.equal('bid12345678'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(360); + }); + + it('Verifies bidder code', function () { + expect(spec.code).to.equal('platformio'); + }); + + it('Verifies supported media types', function () { + expect(spec.supportedMediaTypes).to.have.lengthOf(3); + expect(spec.supportedMediaTypes[0]).to.equal('banner'); + expect(spec.supportedMediaTypes[1]).to.equal('native'); + expect(spec.supportedMediaTypes[2]).to.equal('video'); + }); + + it('Verifies if bid request valid', function () { + expect(spec.isBidRequestValid(slotConfigs[0])).to.equal(true); + expect(spec.isBidRequestValid(slotConfigs[1])).to.equal(true); + expect(spec.isBidRequestValid(nativeSlotConfig[0])).to.equal(true); + expect(spec.isBidRequestValid(videoSlotConfig[0])).to.equal(true); + }); + + it('Verify app requests', function () { + const request = spec.buildRequests(appSlotConfig); + const ortbRequest = JSON.parse(request.data); + expect(ortbRequest.site).to.equal(null); + expect(ortbRequest.app).to.not.be.null; + expect(ortbRequest.app.publisher).to.not.equal(null); + expect(ortbRequest.app.publisher.id).to.equal('29521'); + expect(ortbRequest.app.id).to.equal('1111'); + expect(ortbRequest.app.name).to.equal('app name'); + expect(ortbRequest.app.bundle).to.equal('io.platform.apps'); + expect(ortbRequest.app.storeurl).to.equal('https://platform.io/apps'); + expect(ortbRequest.app.domain).to.equal('platform.io'); + }); + + it('Verify GDPR', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'serialized_gpdr_data' + } + }; + const request = spec.buildRequests(slotConfigs, bidderRequest); + expect(request.url).to.equal('https://piohbdisp.hb.adx1.com/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + expect(ortbRequest.user).to.not.equal(null); + expect(ortbRequest.user.ext).to.not.equal(null); + expect(ortbRequest.user.ext.consent).to.equal('serialized_gpdr_data'); + expect(ortbRequest.regs).to.not.equal(null); + expect(ortbRequest.regs.ext).to.not.equal(null); + expect(ortbRequest.regs.ext.gdpr).to.equal(1); + }); +}); From 190520c7719920326a44a1bc8c799525ff667783 Mon Sep 17 00:00:00 2001 From: r-schweitzer <50628828+r-schweitzer@users.noreply.github.com> Date: Thu, 26 Mar 2020 19:30:55 +0100 Subject: [PATCH 21/90] feature/alias-sync-config-option (#4879) * added config option to enabled alias syncs * added unit test --- src/adapters/bidderFactory.js | 3 +- test/spec/unit/core/bidderFactory_spec.js | 51 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 64bfe27c0c5..3e8771cd8df 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -331,7 +331,8 @@ export function newBidder(spec) { }); function registerSyncs(responses, gdprConsent, uspConsent) { - if (spec.getUserSyncs && !adapterManager.aliasRegistry[spec.code]) { + const aliasSyncEnabled = config.getConfig('userSync.aliasSyncEnabled'); + if (spec.getUserSyncs && (aliasSyncEnabled || !adapterManager.aliasRegistry[spec.code])) { let filterConfig = config.getConfig('userSync.filterSettings'); let syncs = spec.getUserSyncs({ iframeEnabled: !!(filterConfig && (filterConfig.iframe || filterConfig.all)), diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index c06eb29b6b9..03635803b23 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -56,15 +56,66 @@ describe('bidders created by newBidder', function () { describe('when the ajax response is irrelevant', function () { let ajaxStub; + let getConfigSpy; beforeEach(function () { ajaxStub = sinon.stub(ajax, 'ajax'); addBidResponseStub.reset(); + getConfigSpy = sinon.spy(config, 'getConfig'); doneStub.reset(); }); afterEach(function () { ajaxStub.restore(); + getConfigSpy.restore(); + }); + + it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); + + it('should let registerSyncs run with valid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); + + it('should let registerSyncs run with invalid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); + + it('should not let registerSyncs run with valid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false + } + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(false); }); it('should handle bad bid requests gracefully', function () { From cceaf0f1de9581315b1ed217484eb0d2f4f71d9a Mon Sep 17 00:00:00 2001 From: Alex Khmelnitsky Date: Fri, 27 Mar 2020 12:48:56 +0300 Subject: [PATCH 22/90] cedato adapter added support for multiple players (#4990) --- modules/cedatoBidAdapter.js | 49 +++++++++++++++------- test/spec/modules/cedatoBidAdapter_spec.js | 4 +- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/modules/cedatoBidAdapter.js b/modules/cedatoBidAdapter.js index 72377b2dfdb..42a9baa514a 100644 --- a/modules/cedatoBidAdapter.js +++ b/modules/cedatoBidAdapter.js @@ -7,7 +7,6 @@ const BID_URL = 'https://h.cedatoplayer.com/hb'; const SYNC_URL = 'https://h.cedatoplayer.com/hb_usync'; const TTL = 10000; const CURRENCY = 'USD'; -const FIRST_PRICE = 1; const NET_REVENUE = true; export const spec = { @@ -25,11 +24,8 @@ export const spec = { }, buildRequests: function(bidRequests, bidderRequest) { - const req = bidRequests[Math.floor(Math.random() * bidRequests.length)]; - const params = req.params; - const at = FIRST_PRICE; - const site = { id: params.player_id, domain: document.domain }; - const device = { ua: navigator.userAgent }; + const site = { domain: document.domain }; + const device = { ua: navigator.userAgent, w: screen.width, h: screen.height }; const currency = CURRENCY; const tmax = bidderRequest.timeout; const auctionId = bidderRequest.auctionId; @@ -39,7 +35,7 @@ export const spec = { const imp = bidRequests.map(req => { const banner = getMediaType(req, 'banner'); const video = getMediaType(req, 'video'); - const bidfloor = params.bidfloor; + const params = req.params; const bidId = req.bidId; const adUnitCode = req.adUnitCode; const bidRequestsCount = req.bidRequestsCount; @@ -51,16 +47,15 @@ export const spec = { banner, video, adUnitCode, - bidfloor, bidRequestsCount, bidderWinsCount, - transactionId + transactionId, + params }; }); const payload = { version: '$prebid.version$', - at, site, device, imp, @@ -83,12 +78,7 @@ export const spec = { } } - return { - method: 'POST', - url: params.bid_url || BID_URL, - data: JSON.stringify(payload), - bidderRequest - }; + return formatRequest(payload, bidderRequest); }, interpretResponse: function(resp, {bidderRequest}) { @@ -186,6 +176,33 @@ function newBid(serverBid, bidderRequest) { return bid; } +function formatRequest(payload, bidderRequest) { + const payloadByUrl = {}; + const requests = []; + + payload.imp.forEach(imp => { + const url = imp.params.bid_url || BID_URL; + if (!payloadByUrl[url]) { + payloadByUrl[url] = { + ...payload, + imp: [] + }; + } + payloadByUrl[url].imp.push(imp); + }); + + for (const url in payloadByUrl) { + requests.push({ + url, + method: 'POST', + data: JSON.stringify(payloadByUrl[url]), + bidderRequest + }); + } + + return requests; +} + const getSync = (type, gdprConsent, uspConsent = '') => { const syncUrl = SYNC_URL; let params = '?type=' + type + '&us_privacy=' + uspConsent; diff --git a/test/spec/modules/cedatoBidAdapter_spec.js b/test/spec/modules/cedatoBidAdapter_spec.js index bb71f0ff3ea..a7f4875afff 100644 --- a/test/spec/modules/cedatoBidAdapter_spec.js +++ b/test/spec/modules/cedatoBidAdapter_spec.js @@ -50,12 +50,12 @@ describe('the cedato adapter', function () { }); it('should build a very basic request', function() { - var request = spec.buildRequests([bid], bidRequestObj); + var [request] = spec.buildRequests([bid], bidRequestObj); expect(request.method).to.equal('POST'); }); it('should pass gdpr and usp strings to server', function() { - var request = spec.buildRequests([bid], bidRequestObj); + var [request] = spec.buildRequests([bid], bidRequestObj); var payload = JSON.parse(request.data); expect(payload.gdpr_consent).to.not.be.undefined; expect(payload.gdpr_consent.consent_string).to.equal(bidRequestObj.gdprConsent.consentString); From 049617a5ed4d96896d0af1162b8b1f915b8d02a7 Mon Sep 17 00:00:00 2001 From: Valentin Souche Date: Fri, 27 Mar 2020 23:15:14 +0100 Subject: [PATCH 23/90] Handle TCF 2 (#5000) --- modules/teadsBidAdapter.js | 42 +++------ test/spec/modules/teadsBidAdapter_spec.js | 108 ++++++++-------------- 2 files changed, 48 insertions(+), 102 deletions(-) diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index c0f11dc4639..a683eb85cf6 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -54,10 +54,11 @@ export const spec = { if (bidderRequest && gdpr) { let isCmp = (typeof gdpr.gdprApplies === 'boolean') let isConsentString = (typeof gdpr.consentString === 'string') - let status = isCmp ? findGdprStatus(gdpr.gdprApplies, gdpr.vendorData) : gdprStatus.CMP_NOT_FOUND_OR_ERROR + let status = isCmp ? findGdprStatus(gdpr) : gdprStatus.CMP_NOT_FOUND_OR_ERROR payload.gdpr_iab = { consent: isConsentString ? gdpr.consentString : '', - status: status + status: status, + apiVersion: gdpr.apiVersion }; } @@ -101,33 +102,6 @@ export const spec = { } return bidResponses; }, - - getUserSyncs: function(syncOptions, responses, gdprConsent) { - let queryParams = { - hb_provider: 'prebid', - hb_version: '$prebid.version$' - }; - - if (gdprConsent) { - let gdprIab = { - status: findGdprStatus(gdprConsent.gdprApplies, gdprConsent.vendorData), - consent: gdprConsent.consentString - }; - - queryParams.gdprIab = JSON.stringify(gdprIab) - } - - if (utils.deepAccess(responses[0], 'body.responses.0.placementId')) { - queryParams.placementId = responses[0].body.responses[0].placementId - }; - - if (syncOptions.iframeEnabled) { - return [{ - type: 'iframe', - url: 'https://sync.teads.tv/iframe?' + utils.parseQueryStringParameters(queryParams) - }]; - } - } }; function getReferrerInfo(bidderRequest) { @@ -138,11 +112,17 @@ function getReferrerInfo(bidderRequest) { return ref; } -function findGdprStatus(gdprApplies, gdprData) { +function findGdprStatus(gdprConsent) { + const gdprApplies = gdprConsent.gdprApplies + const gdprData = gdprConsent.vendorData + const apiVersion = gdprConsent.apiVersion let status = gdprStatus.GDPR_APPLIES_PUBLISHER; + const globalConsent = apiVersion === 1 + ? (gdprData.hasGlobalScope || gdprData.hasGlobalConsent) + : !gdprData.isServiceSpecific if (gdprApplies) { - if (gdprData.hasGlobalScope || gdprData.hasGlobalConsent) status = gdprStatus.GDPR_APPLIES_GLOBAL + if (globalConsent) status = gdprStatus.GDPR_APPLIES_GLOBAL } else status = gdprStatus.GDPR_DOESNT_APPLY return status; } diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index ea3b7fed967..d424e998994 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -136,7 +136,8 @@ describe('teadsBidAdapter', () => { 'gdprApplies': true, 'vendorData': { 'hasGlobalConsent': false - } + }, + 'apiVersion': 1 } }; @@ -146,6 +147,7 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab).to.exist; expect(payload.gdpr_iab.consent).to.equal(consentString); expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(1); }); it('should add referer info to payload', function () { @@ -175,7 +177,8 @@ describe('teadsBidAdapter', () => { 'gdprApplies': true, 'vendorData': { 'hasGlobalScope': true - } + }, + 'apiVersion': 1 } }; @@ -185,6 +188,32 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab).to.exist; expect(payload.gdpr_iab.consent).to.equal(consentString); expect(payload.gdpr_iab.status).to.equal(11); + expect(payload.gdpr_iab.apiVersion).to.equal(1); + }); + + it('should send GDPR TCF2 to endpoint with 12 status', function() { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(2); }); it('should send GDPR to endpoint with 22 status', function() { @@ -196,7 +225,8 @@ describe('teadsBidAdapter', () => { 'gdprConsent': { 'consentString': undefined, 'gdprApplies': undefined, - 'vendorData': undefined + 'vendorData': undefined, + 'apiVersion': 1 } }; @@ -206,6 +236,7 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab).to.exist; expect(payload.gdpr_iab.consent).to.equal(''); expect(payload.gdpr_iab.status).to.equal(22); + expect(payload.gdpr_iab.apiVersion).to.equal(1); }); it('should send GDPR to endpoint with 0 status', function() { @@ -219,7 +250,8 @@ describe('teadsBidAdapter', () => { 'gdprApplies': false, 'vendorData': { 'hasGlobalScope': false - } + }, + 'apiVersion': 1 } }; @@ -229,6 +261,7 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab).to.exist; expect(payload.gdpr_iab.consent).to.equal(consentString); expect(payload.gdpr_iab.status).to.equal(0); + expect(payload.gdpr_iab.apiVersion).to.equal(1); }); it('should use good mediaTypes video playerSizes', function() { @@ -368,71 +401,4 @@ describe('teadsBidAdapter', () => { expect(result.length).to.equal(0); }); }); - - it('should call userSync with good params', function() { - let bids = [{ - 'body': { - 'responses': [{ - 'ad': ' - + diff --git a/modules/digiTrustIdSystem.js b/modules/digiTrustIdSystem.js index 9d0b5c25e1b..eeb1a4f3b60 100644 --- a/modules/digiTrustIdSystem.js +++ b/modules/digiTrustIdSystem.js @@ -46,7 +46,14 @@ var isFunc = function (fn) { return typeof (fn) === 'function'; } +var _savedId = null; // closure variable for storing Id to avoid additional requests + function callApi(options) { + var ajaxOptions = { + method: 'GET', + withCredentials: true + }; + ajax( DT_ID_SVC, { @@ -54,9 +61,7 @@ function callApi(options) { error: options.fail }, null, - { - method: 'GET' - } + ajaxOptions ); } @@ -91,8 +96,6 @@ function writeDigiId(id) { * */ function initDigitrustFacade(config) { - var _savedId = null; // closure variable for storing Id to avoid additional requests - clearTimeout(fallbackTimer); fallbackTimer = 0; @@ -116,7 +119,7 @@ function initDigitrustFacade(config) { try { inter.initCallback(idResponse); } catch (ex) { - utils.logError('Exception in passed DigiTrust init callback'); + utils.logError('Exception in passed DigiTrust init callback', ex); } } } @@ -146,9 +149,9 @@ function initDigitrustFacade(config) { success: true } try { - writeDigiId(respText); idResult.identity = JSON.parse(respText); - _savedId = idResult; + _savedId = idResult; // Save result to the cache variable + writeDigiId(respText); } catch (ex) { idResult.success = false; delete idResult.identity; @@ -253,6 +256,10 @@ var ResultWrapper = function (opts) { */ this.userIdCallback = function (callback) { idSystemFn = callback; + if (me.idObj == null) { + me.idObj = _savedId; + } + if (me.idObj != null && isFunc(callback)) { callback(wrapIdResult()); } @@ -262,6 +269,10 @@ var ResultWrapper = function (opts) { * Return a wrapped result formatted for userId system */ function wrapIdResult() { + if (me.idObj == null) { + me.idObj = _savedId; + } + if (me.idObj == null) { return null; } From 0bddfd50651554e975106fa1bc24fc0b3f2c61b9 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Tue, 31 Mar 2020 11:11:03 -0700 Subject: [PATCH 32/90] Pubmatic: disable test-suit refreshing test-case (#5049) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * disabled a test case causing test-suite refresh --- test/spec/modules/pubmaticBidAdapter_spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 75c0beed995..62dd76f04b6 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -725,7 +725,9 @@ describe('PubMatic adapter', function () { expect(data.test).to.equal(undefined); }); - it('test flag set to 1 when pubmaticTest=true is present in page url', function() { + // disabled this test case as it refreshes the whole suite when in karma watch mode + // todo: needs a fix + xit('test flag set to 1 when pubmaticTest=true is present in page url', function() { window.location.href += '#pubmaticTest=true'; // now all the test cases below will have window.location.href with #pubmaticTest=true let request = spec.buildRequests(bidRequests); From a23ce02b172a53dfc89010c18fa7fa815c722d8f Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Tue, 31 Mar 2020 11:59:00 -0700 Subject: [PATCH 33/90] Price Floors (#4931) * Price Floors (#52) * Initial Commit Code cleanup + PBS Changes + tests still needed * Refactor to remove event * forgot to remove FLOOR_NOT_MET * Clean up code * broke out req params * floorNotMet is not bidRejected * clean up module, more requirements on bid and analytics adapters moved gpt matching into a util function * test updates + currency conversion now on global * asdf * floor data object schema changed updated to object not array * Selection function now needs to sort by number of *'s in the rule Rubicon get video floor use actual player size * Updated json schema * tests finished * domain is more sophisticated now * Currency floors refactored a little Added log for when bid is floored * update tests after additional data passed to analytics * analytics reqs updated * test update * update to cache lookups Analytics adapter update tests updated for cache lookup * reverting temp stuff * Pulling latest from master = few probs Small currency bug * revert newline * improved rule selection for getFloor * new api for adding new fields overrides if debug on, resp hook after debug hook small bug in overwriting data object * handle adjustment factor of 0 * removed size and mt specific logic and pushed to matching func * adding comments for all funcs * Added tests for rubicon adapters handle matchign keys with dot's * undefined data should be ignored * remove unnecessary -1 plus semi colon * md file * removing temp test file * prebid uses native image size as a single array so we wrap it * Forgot IE 11 no have includes! * if enforcement not passed in default it Exact match not found, then * * no need to decode uri entire url when just wanting hostname * setting auctionId on bidRequests so `onBeforeRequestBids` has context for getFloor() * rubi analytics new values * js rounding fix + isNaN handling * random parenthesis * fixing formula Co-authored-by: rmartinez --- gulpfile.js | 4 +- modules/currency.js | 4 + modules/priceFloors.js | 645 ++++++++++++ modules/priceFloors.md | 62 ++ modules/rubiconAnalyticsAdapter.js | 70 +- modules/rubiconBidAdapter.js | 22 +- src/constants.json | 3 +- src/utils.js | 18 + test/spec/modules/currency_spec.js | 40 +- test/spec/modules/priceFloorsSchema.json | 86 ++ test/spec/modules/priceFloors_spec.js | 972 ++++++++++++++++++ .../modules/rubiconAnalyticsAdapter_spec.js | 96 ++ test/spec/modules/rubiconAnalyticsSchema.json | 3 +- test/spec/modules/rubiconBidAdapter_spec.js | 75 +- 14 files changed, 2082 insertions(+), 18 deletions(-) create mode 100644 modules/priceFloors.js create mode 100644 modules/priceFloors.md create mode 100644 test/spec/modules/priceFloorsSchema.json create mode 100644 test/spec/modules/priceFloors_spec.js diff --git a/gulpfile.js b/gulpfile.js index 111bd9dc3bb..1b5cd85abd6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -249,13 +249,13 @@ function test(done) { execa(wdioCmd, wdioOpts, { stdio: 'inherit' }) .then(stdout => { - //kill mock server + // kill mock server mockServer.kill('SIGINT'); done(); process.exit(0); }) .catch(err => { - //kill mock server + // kill mock server mockServer.kill('SIGINT'); done(new Error(`Tests failed with error: ${err}`)); process.exit(1); diff --git a/modules/currency.js b/modules/currency.js index 84ca5e42783..0464d9b5cdb 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -1,3 +1,4 @@ +import { getGlobal } from '../src/prebidGlobal.js'; import { createBid } from '../src/bidfactory.js'; import { STATUS } from '../src/constants.json'; import { ajax } from '../src/ajax.js'; @@ -122,6 +123,8 @@ function initCurrency(url) { utils.logInfo('Installing addBidResponse decorator for currency module', arguments); + // Adding conversion function to prebid global for external module and on page use + getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); getHook('addBidResponse').before(addBidResponseHook, 100); // call for the file if we haven't already @@ -149,6 +152,7 @@ function resetCurrency() { utils.logInfo('Uninstalling addBidResponse decorator for currency module', arguments); getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + delete getGlobal().convertCurrency; adServerCurrency = 'USD'; conversionCache = {}; diff --git a/modules/priceFloors.js b/modules/priceFloors.js new file mode 100644 index 00000000000..2ee5fb3b0fd --- /dev/null +++ b/modules/priceFloors.js @@ -0,0 +1,645 @@ +import { getGlobal } from '../src/prebidGlobal.js'; +import { config } from '../src/config.js'; +import * as utils from '../src/utils.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; +import { getHook } from '../src/hook.js'; +import { createBid } from '../src/bidfactory.js'; +import find from 'core-js/library/fn/array/find.js'; +import { parse as urlParse } from '../src/url.js'; +import { getRefererInfo } from '../src/refererDetection.js'; + +/** + * @summary This Module is intended to provide users with the ability to dynamically set and enforce price floors on a per auction basis. + */ +const MODULE_NAME = 'Price Floors'; + +/** + * @summary Instantiate Ajax so we control the timeout + */ +const ajax = ajaxBuilder(10000); + +/** + * @summary Allowed fields for rules to have + */ +export let allowedFields = ['gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType']; + +/** + * @summary This is a flag to indicate if a AJAX call is processing for a floors request +*/ +let fetching = false; + +/** + * @summary so we only register for our hooks once +*/ +let addedFloorsHook = false; + +/** + * @summary The config to be used. Can be updated via: setConfig or a real time fetch + */ +let _floorsConfig = {}; + +/** + * @summary If a auction is to be delayed by an ongoing fetch we hold it here until it can be resumed + */ +let _delayedAuctions = []; + +/** + * @summary Each auction can have differing floors data depending on execution time or per adunit setup + * So we will be saving each auction offset by it's auctionId in order to make sure data is not changed + * Once the auction commences + */ +export let _floorDataForAuction = {}; + +/** + * @summary Simple function to round up to a certain decimal degree + */ +function roundUp(number, precision) { + return Math.ceil(parseFloat(number) * Math.pow(10, precision)) / Math.pow(10, precision); +} + +let referrerHostname; +function getHostNameFromReferer(referer) { + referrerHostname = urlParse(referer, {noDecodeWholeURL: true}).hostname; + return referrerHostname; +} + +/** + * @summary floor field types with their matching functions to resolve the actual matched value + */ +export let fieldMatchingFunctions = { + 'size': (bidRequest, bidResponse) => utils.parseGPTSingleSizeArray(bidResponse.size) || '*', + 'mediaType': (bidRequest, bidResponse) => bidResponse.mediaType || 'banner', + 'gptSlot': (bidRequest, bidResponse) => utils.getGptSlotInfoForAdUnitCode(bidRequest.adUnitCode).gptSlot, + 'domain': (bidRequest, bidResponse) => referrerHostname || getHostNameFromReferer(getRefererInfo().referer), + 'adUnitCode': (bidRequest, bidResponse) => bidRequest.adUnitCode +} + +/** + * @summary Based on the fields array in floors data, it enumerates all possible matches based on exact match coupled with + * a "*" catch-all match + * Returns array of Tuple [exact match, catch all] for each field in rules file + */ +function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) { + // generate combination of all exact matches and catch all for each field type + return floorFields.reduce((accum, field) => { + let exactMatch = fieldMatchingFunctions[field](bidObject, responseObject) || '*'; + // storing exact matches as lowerCase since we want to compare case insensitively + accum.push(exactMatch === '*' ? ['*'] : [exactMatch.toLowerCase(), '*']); + return accum; + }, []); +} + +/** + * @summary get's the first matching floor based on context provided. + * Generates all possible rule matches and picks the first matching one. + */ +export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) { + let fieldValues = enumeratePossibleFieldValues(utils.deepAccess(floorData, 'schema.fields') || [], bidObject, responseObject); + if (!fieldValues.length) return { matchingFloor: floorData.default }; + + // look to see iof a request for this context was made already + let matchingInput = fieldValues.map(field => field[0]).join('-'); + // if we already have gotten the matching rule from this matching input then use it! No need to look again + let previousMatch = utils.deepAccess(floorData, `matchingInputs.${matchingInput}`); + if (previousMatch) { + return previousMatch; + } + let allPossibleMatches = generatePossibleEnumerations(fieldValues, utils.deepAccess(floorData, 'schema.delimiter') || '|'); + let matchingRule = find(allPossibleMatches, hashValue => floorData.values.hasOwnProperty(hashValue)); + + let matchingData = { + matchingFloor: floorData.values[matchingRule] || floorData.default, + matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters + matchingRule + }; + // save for later lookup if needed + utils.deepSetValue(floorData, `matchingInputs.${matchingInput}`, {...matchingData}); + return matchingData; +} + +/** + * @summary Generates all possible rule hash's based on input array of array's + * The generated list is of all possible key matches based on fields input + * The list is sorted by least amount of * in rule to most with left most fields taking precedence + */ +function generatePossibleEnumerations(arrayOfFields, delimiter) { + return arrayOfFields.reduce((accum, currentVal) => { + let ret = []; + accum.map(obj => { + currentVal.map(obj1 => { + ret.push(obj + delimiter + obj1) + }); + }); + return ret; + }).sort((left, right) => left.split('*').length - right.split('*').length); +} + +/** + * @summary If a the input bidder has a registered cpmadjustment it returns the input CPM after being adjusted + */ +export function getBiddersCpmAdjustment(bidderName, inputCpm) { + const adjustmentFunction = utils.deepAccess(getGlobal(), `bidderSettings.${bidderName}.bidCpmAdjustment`); + if (adjustmentFunction) { + return parseFloat(adjustmentFunction(inputCpm)); + } + return parseFloat(inputCpm); +} + +/** + * @summary This function takes the original floor and the adjusted floor in order to determine the bidders actual floor + * With js rounding errors with decimal division we utilize similar method as shown in cpmBucketManager.js + */ +export function calculateAdjustedFloor(oldFloor, newFloor) { + const pow = Math.pow(10, 10); + return ((oldFloor * pow) / (newFloor * pow) * (oldFloor * pow)) / pow; +} + +/** + * @summary gets the prebid set sizes depending on the input mediaType + */ +const getMediaTypesSizes = { + banner: (bid) => utils.deepAccess(bid, 'mediaTypes.banner.sizes') || [], + video: (bid) => utils.deepAccess(bid, 'mediaTypes.video.playerSize') || [], + native: (bid) => utils.deepAccess(bid, 'mediaTypes.native.image.sizes') ? [utils.deepAccess(bid, 'mediaTypes.native.image.sizes')] : [] +} + +/** + * @summary for getFloor only, before selecting a rule, if a bidAdapter asks for * in their getFloor params + * Then we may be able to get a better rule than the * ones depending on context of the adUnit + */ +function updateRequestParamsFromContext(bidRequest, requestParams) { + // if adapter asks for *'s then we can do some logic to infer if we can get a more specific rule based on context of bid + let mediaTypesOnBid = Object.keys(bidRequest.mediaTypes || {}); + // if there is only one mediaType then we can just use it + if (requestParams.mediaType === '*' && mediaTypesOnBid.length === 1) { + requestParams.mediaType = mediaTypesOnBid[0]; + } + // if they asked for * size, but for the given mediaType there is only one size, we can just use it + if (requestParams.size === '*' && mediaTypesOnBid.indexOf(requestParams.mediaType) !== -1 && getMediaTypesSizes[requestParams.mediaType] && getMediaTypesSizes[requestParams.mediaType](bidRequest).length === 1) { + requestParams.size = getMediaTypesSizes[requestParams.mediaType](bidRequest)[0]; + } + return requestParams; +} + +/** + * @summary This is the function which will return a single floor based on the input requests + * and matching it to a rule for the current auction + */ +export function getFloor(requestParams = {currency: 'USD', mediaType: '*', size: '*'}) { + let bidRequest = this; + let floorData = _floorDataForAuction[bidRequest.auctionId]; + if (!floorData || floorData.skipped) return {}; + + requestParams = updateRequestParamsFromContext(bidRequest, requestParams); + let floorInfo = getFirstMatchingFloor(floorData.data, {...bidRequest}, {mediaType: requestParams.mediaType, size: requestParams.size}); + let currency = requestParams.currency || floorData.data.currency; + + // if bidder asked for a currency which is not what floors are set in convert + if (floorInfo.matchingFloor && currency !== floorData.data.currency) { + try { + floorInfo.matchingFloor = getGlobal().convertCurrency(floorInfo.matchingFloor, floorData.data.currency, currency); + } catch (err) { + utils.logWarn(`${MODULE_NAME}: Unable to get currency conversion for getFloor for bidder ${bidRequest.bidder}. You must have currency module enabled with defaultRates in your currency config`); + // since we were unable to convert to the bidders requested currency, we send back just the actual floors currency to them + currency = floorData.data.currency; + } + } + + // if cpmAdjustment flag is true and we have a valid floor then run the adjustment on it + if (floorData.enforcement.bidAdjustment && floorInfo.matchingFloor) { + let cpmAdjustment = getBiddersCpmAdjustment(bidRequest.bidder, floorInfo.matchingFloor); + floorInfo.matchingFloor = cpmAdjustment ? calculateAdjustedFloor(floorInfo.matchingFloor, cpmAdjustment) : floorInfo.matchingFloor; + } + + if (floorInfo.matchingFloor) { + return { + floor: roundUp(floorInfo.matchingFloor, 4), + currency, + }; + } + return {}; +} + +/** + * @summary Takes a floorsData object and converts it into a hash map with appropriate keys + */ +export function getFloorsDataForAuction(floorData, adUnitCode) { + let auctionFloorData = utils.deepClone(floorData); + auctionFloorData.schema.delimiter = floorData.schema.delimiter || '|'; + auctionFloorData.values = normalizeRulesForAuction(auctionFloorData, adUnitCode); + // default the currency to USD if not passed in + auctionFloorData.currency = auctionFloorData.currency || 'USD'; + return auctionFloorData; +} + +/** + * @summary if adUnitCode needs to be added to the offset then it will add it else just return the values + */ +function normalizeRulesForAuction(floorData, adUnitCode) { + let fields = floorData.schema.fields; + let delimiter = floorData.schema.delimiter + + // if we are building the floor data form an ad unit, we need to append adUnit code as to not cause collisions + let prependAdUnitCode = adUnitCode && fields.indexOf('adUnitCode') === -1 && fields.unshift('adUnitCode'); + return Object.keys(floorData.values).reduce((rulesHash, oldKey) => { + let newKey = prependAdUnitCode ? `${adUnitCode}${delimiter}${oldKey}` : oldKey + // we store the rule keys as lower case for case insensitive compare + rulesHash[newKey.toLowerCase()] = floorData.values[oldKey]; + return rulesHash; + }, {}); +} + +/** + * @summary This function will take the adUnits and generate a floor data object to be used during the auction + * Only called if no set config or fetch level data has returned + */ +export function getFloorDataFromAdUnits(adUnits) { + return adUnits.reduce((accum, adUnit) => { + if (isFloorsDataValid(adUnit.floors)) { + // if values already exist we want to not overwrite them + if (!accum.values) { + accum = getFloorsDataForAuction(adUnit.floors, adUnit.code); + accum.location = 'adUnit'; + } else { + let newRules = getFloorsDataForAuction(adUnit.floors, adUnit.code).values; + // copy over the new rules into our values object + Object.assign(accum.values, newRules); + } + } + return accum; + }, {}); +} + +/** + * @summary This function takes the adUnits for the auction and update them accordingly as well as returns the rules hashmap for the auction + */ +export function updateAdUnitsForAuction(adUnits, floorData, skipped, auctionId) { + adUnits.forEach((adUnit) => { + adUnit.bids.forEach(bid => { + if (skipped) { + delete bid.getFloor; + } else { + bid.getFloor = getFloor; + } + // information for bid and analytics adapters + bid.auctionId = auctionId; + bid.floorData = { + skipped, + modelVersion: utils.deepAccess(floorData, 'data.modelVersion') || '', + location: floorData.data.location, + } + }); + }); +} + +/** + * @summary Updates the adUnits accordingly and returns the necessary floorsData for the current auction + */ +export function createFloorsDataForAuction(adUnits, auctionId) { + let resolvedFloorsData = utils.deepClone(_floorsConfig); + + // if we do not have a floors data set, we will try to use data set on adUnits + let useAdUnitData = Object.keys(utils.deepAccess(resolvedFloorsData, 'data.values') || {}).length === 0; + if (useAdUnitData) { + resolvedFloorsData.data = getFloorDataFromAdUnits(adUnits); + } else { + resolvedFloorsData.data = getFloorsDataForAuction(resolvedFloorsData.data); + } + // if we still do not have a valid floor data then floors is not on for this auction + if (Object.keys(utils.deepAccess(resolvedFloorsData, 'data.values') || {}).length === 0) { + return; + } + // determine the skip rate now + const isSkipped = Math.random() * 100 < parseFloat(utils.deepAccess(resolvedFloorsData, 'data.skipRate') || 0); + resolvedFloorsData.skipped = isSkipped; + updateAdUnitsForAuction(adUnits, resolvedFloorsData, isSkipped, auctionId); + return resolvedFloorsData; +} + +/** + * @summary This is the function which will be called to exit our module and continue the auction. + */ +export function continueAuction(hookConfig) { + // only run if hasExited + if (!hookConfig.hasExited) { + // if this current auction is still fetching, remove it from the _delayedAuctions + _delayedAuctions = _delayedAuctions.filter(auctionConfig => auctionConfig.timer !== hookConfig.timer); + + // We need to know the auctionId at this time. So we will use the passed in one or generate and set it ourselves + hookConfig.reqBidsConfigObj.auctionId = hookConfig.reqBidsConfigObj.auctionId || utils.generateUUID(); + + // now we do what we need to with adUnits and save the data object to be used for getFloor and enforcement calls + _floorDataForAuction[hookConfig.reqBidsConfigObj.auctionId] = createFloorsDataForAuction(hookConfig.reqBidsConfigObj.adUnits || getGlobal().adUnits, hookConfig.reqBidsConfigObj.auctionId); + + hookConfig.nextFn.apply(hookConfig.context, [hookConfig.reqBidsConfigObj]); + hookConfig.hasExited = true; + } +} + +function validateSchemaFields(fields) { + if (Array.isArray(fields) && fields.length > 0 && fields.every(field => allowedFields.indexOf(field) !== -1)) { + return true; + } + utils.logError(`${MODULE_NAME}: Fields recieved do not match allowed fields`); + return false; +} + +function isValidRule(key, floor, numFields, delimiter) { + if (typeof key !== 'string' || key.split(delimiter).length !== numFields) { + return false; + } + return typeof floor === 'number'; +} + +function validateRules(floorsData, numFields, delimiter) { + if (typeof floorsData.values !== 'object') { + return false; + } + // if an invalid rule exists we remove it + floorsData.values = Object.keys(floorsData.values).reduce((filteredRules, key) => { + if (isValidRule(key, floorsData.values[key], numFields, delimiter)) { + filteredRules[key] = floorsData.values[key]; + } + return filteredRules + }, {}); + // rules is only valid if at least one rule remains + return Object.keys(floorsData.values).length > 0; +} + +/** + * @summary Fields array should have at least one entry and all should match allowed fields + * Each rule in the values array should have a 'key' and 'floor' param + * And each 'key' should have the correct number of 'fields' after splitting + * on the delim. If rule does not match remove it. return if still at least 1 rule + */ +export function isFloorsDataValid(floorsData) { + if (typeof floorsData !== 'object') { + return false; + } + // schema.fields has only allowed attributes + if (!validateSchemaFields(utils.deepAccess(floorsData, 'schema.fields'))) { + return false; + } + return validateRules(floorsData, floorsData.schema.fields.length, floorsData.schema.delimiter || '|') +} + +/** + * @summary This function updates the global Floors Data field based on the new one passed in if it is valid + */ +export function parseFloorData(floorsData, location) { + if (floorsData && typeof floorsData === 'object' && isFloorsDataValid(floorsData)) { + return { + ...floorsData, + location + }; + } + utils.logError(`${MODULE_NAME}: The floors data did not contain correct values`, floorsData); +} + +/** + * + * @param {Object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. + * @param {function} fn required; The next function in the chain, used by hook.js + */ +export function requestBidsHook(fn, reqBidsConfigObj) { + // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions) + const hookConfig = { + reqBidsConfigObj, + context: this, + nextFn: fn, + haveExited: false, + timer: null + }; + + // If auction delay > 0 AND we are fetching -> Then wait until it finishes + if (_floorsConfig.auctionDelay > 0 && fetching) { + hookConfig.timer = setTimeout(() => { + utils.logWarn(`${MODULE_NAME}: Fetch attempt did not return in time for auction`); + continueAuction(hookConfig); + }, _floorsConfig.auctionDelay); + _delayedAuctions.push(hookConfig); + } else { + continueAuction(hookConfig); + } +} + +/** + * @summary If an auction was queued to be delayed (waiting for a fetch) then this function will resume + * those delayed auctions when delay is hit or success return or fail return + */ +function resumeDelayedAuctions() { + _delayedAuctions.forEach(auctionConfig => { + // clear the timeout + clearTimeout(auctionConfig.timer); + continueAuction(auctionConfig); + }); + _delayedAuctions = []; +} + +/** + * This function handles the ajax response which comes from the user set URL to fetch floors data from + * @param {object} fetchResponse The floors data response which came back from the url configured in config.floors + */ +export function handleFetchResponse(fetchResponse) { + fetching = false; + let floorResponse; + try { + floorResponse = JSON.parse(fetchResponse); + } catch (ex) { + floorResponse = fetchResponse; + } + // Update the global floors object according to the fetched data + _floorsConfig.data = parseFloorData(floorResponse, 'fetch') || _floorsConfig.data; + + // if any auctions are waiting for fetch to finish, we need to continue them! + resumeDelayedAuctions(); +} + +function handleFetchError(status) { + fetching = false; + utils.logError(`${MODULE_NAME}: Fetch errored with: ${status}`); + + // if any auctions are waiting for fetch to finish, we need to continue them! + resumeDelayedAuctions(); +} + +/** + * This function handles sending and recieving the AJAX call for a floors fetch + * @param {object} floorsConfig the floors config coming from setConfig + */ +export function generateAndHandleFetch(floorEndpoint) { + // if a fetch url is defined and one is not already occuring, fire it! + if (floorEndpoint.url && !fetching) { + // default to GET and we only support GET for now + let requestMethod = floorEndpoint.method || 'GET'; + if (requestMethod !== 'GET') { + utils.logError(`${MODULE_NAME}: 'GET' is the only request method supported at this time!`); + } else { + ajax(floorEndpoint.url, { success: handleFetchResponse, error: handleFetchError }, null, { method: 'GET' }); + fetching = true; + } + } else if (fetching) { + utils.logWarn(`${MODULE_NAME}: A fetch is already occuring. Skipping.`); + } +} + +/** + * @summary Updates our allowedFields and fieldMatchingFunctions with the publisher defined new ones + */ +function addFieldOverrides(overrides) { + Object.keys(overrides).forEach(override => { + // we only add it if it is not already in the allowed fields and if the passed in value is a function + if (allowedFields.indexOf(override) === -1 && typeof overrides[override] === 'function') { + allowedFields.push(override); + fieldMatchingFunctions[override] = overrides[override]; + } + }); +} + +/** + * @summary This is the function which controls what happens during a pbjs.setConfig({...floors: {}}) is called + */ +export function handleSetFloorsConfig(config) { + _floorsConfig = utils.pick(config, [ + 'enabled', enabled => enabled !== false, // defaults to true + 'auctionDelay', auctionDelay => auctionDelay || 0, + 'endpoint', endpoint => endpoint || {}, + 'enforcement', enforcement => utils.pick(enforcement || {}, [ + 'enforceJS', enforceJS => enforceJS !== false, // defaults to true + 'enforcePBS', enforcePBS => enforcePBS === true, // defaults to false + 'floorDeals', floorDeals => floorDeals === true, // defaults to false + 'bidAdjustment', bidAdjustment => bidAdjustment !== false, // defaults to true + ]), + 'additionalSchemaFields', additionalSchemaFields => typeof additionalSchemaFields === 'object' && Object.keys(additionalSchemaFields).length > 0 ? addFieldOverrides(additionalSchemaFields) : undefined, + 'data', data => (data && parseFloorData(data, 'setConfig')) || _floorsConfig.data // do not overwrite if passed in data not valid + ]); + + // if enabled then do some stuff + if (_floorsConfig.enabled) { + // handle the floors fetch + generateAndHandleFetch(_floorsConfig.endpoint); + + if (!addedFloorsHook) { + // register hooks / listening events + // when auction finishes remove it's associated floor data + events.on(CONSTANTS.EVENTS.AUCTION_END, (args) => delete _floorDataForAuction[args.auctionId]); + + // we want our hooks to run after the currency hooks + getGlobal().requestBids.before(requestBidsHook, 50); + // if user has debug on then we want to allow the debugging module to run before this, assuming they are testing priceFloors + // debugging is currently set at 5 priority + getHook('addBidResponse').before(addBidResponseHook, utils.debugTurnedOn() ? 4 : 50); + addedFloorsHook = true; + } + } else { + utils.logInfo(`${MODULE_NAME}: Turning off module`); + + _floorsConfig = {}; + _floorDataForAuction = {}; + + getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + getGlobal().requestBids.getHooks({hook: requestBidsHook}).remove(); + + addedFloorsHook = false; + } +} + +/** + * @summary Analytics adapters especially need context of what the floors module is doing in order + * to best create informed models. This function attaches necessary information to the bidResponse object for processing + */ +function addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm) { + bid.floorData = { + floorValue: floorInfo.matchingFloor, + floorRule: floorInfo.matchingRule, + floorCurrency: floorData.data.currency, + cpmAfterAdjustments: adjustedCpm, + enforcements: {...floorData.enforcement}, + matchedFields: {} + }; + floorData.data.schema.fields.forEach((field, index) => { + let matchedValue = floorInfo.matchingData.split(floorData.data.schema.delimiter)[index]; + bid.floorData.matchedFields[field] = matchedValue; + }); +} + +/** + * @summary takes the enforcement flags and the bid itself and determines if it should be floored + */ +function shouldFloorBid(floorData, floorInfo, bid) { + let enforceJS = utils.deepAccess(floorData, 'enforcement.enforceJS') !== false; + let shouldFloorDeal = utils.deepAccess(floorData, 'enforcement.floorDeals') === true || !bid.dealId; + let bidBelowFloor = bid.floorData.cpmAfterAdjustments < floorInfo.matchingFloor; + return enforceJS && (bidBelowFloor && shouldFloorDeal); +} + +/** + * @summary The main driving force of floors. On bidResponse we hook in and intercept bidResponses. + * And if the rule we find determines a bid should be floored we will do so. + */ +export function addBidResponseHook(fn, adUnitCode, bid) { + let floorData = _floorDataForAuction[this.bidderRequest.auctionId]; + // if no floor data or associated bidRequest then bail + const matchingBidRequest = find(this.bidderRequest.bids, bidRequest => bidRequest.bidId && bidRequest.bidId === bid.requestId); + if (!floorData || !bid || floorData.skipped || !matchingBidRequest) { + return fn.call(this, adUnitCode, bid); + } + + // get the matching rule + let floorInfo = getFirstMatchingFloor(floorData.data, {...matchingBidRequest}, {...bid, size: [bid.width, bid.height]}); + + if (!floorInfo.matchingFloor) { + utils.logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); + return fn.call(this, adUnitCode, bid); + } + + // determine the base cpm to use based on if the currency matches the floor currency + let adjustedCpm; + let floorCurrency = floorData.data.currency.toUpperCase(); + let bidResponseCurrency = bid.currency || 'USD'; // if an adapter does not set a bid currency and currency module not on it may come in as undefined + if (floorCurrency === bidResponseCurrency.toUpperCase()) { + adjustedCpm = bid.cpm; + } else if (bid.originalCurrency && floorCurrency === bid.originalCurrency.toUpperCase()) { + adjustedCpm = bid.originalCpm; + } else { + try { + adjustedCpm = getGlobal().convertCurrency(bid.cpm, bidResponseCurrency.toUpperCase(), floorCurrency); + } catch (err) { + utils.logError(`${MODULE_NAME}: Unable do get currency conversion for bidResponse to Floor Currency. Do you have Currency module enabled? ${bid}`); + return fn.call(this, adUnitCode, bid); + } + } + + // ok we got the bid response cpm in our desired currency. Now we need to run the bidders CPMAdjustment function if it exists + adjustedCpm = getBiddersCpmAdjustment(bid.bidderCode, adjustedCpm); + + // add necessary data information for analytics adapters / floor providers would possibly need + addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm); + + // now do the compare! + if (shouldFloorBid(floorData, floorInfo, bid)) { + // bid fails floor -> throw it out + // create basic bid no-bid with necessary data fro analytics adapters + let flooredBid = createBid(CONSTANTS.STATUS.NO_BID, matchingBidRequest); + Object.assign(flooredBid, utils.pick(bid, [ + 'floorData', + 'width', + 'height', + 'mediaType', + 'currency', + 'originalCpm', + 'originalCurrency', + 'getCpmInNewCurrency', + ])); + flooredBid.status = CONSTANTS.BID_STATUS.BID_REJECTED; + // if floor not met update bid with 0 cpm so it is not included downstream and marked as no-bid + flooredBid.cpm = 0; + utils.logWarn(`${MODULE_NAME}: ${flooredBid.bidderCode}'s Bid Response for ${adUnitCode} was rejected due to floor not met`, bid); + return fn.call(this, adUnitCode, flooredBid); + } + return fn.call(this, adUnitCode, bid); +} + +config.getConfig('floors', config => handleSetFloorsConfig(config.floors)); diff --git a/modules/priceFloors.md b/modules/priceFloors.md new file mode 100644 index 00000000000..d09be78c620 --- /dev/null +++ b/modules/priceFloors.md @@ -0,0 +1,62 @@ +## Dynamic Price Floors + +### Setup +```javascript +pbjs.setConfig({ + floors: { + enabled: true, //defaults to true + enforcement: { + floorDeals: false, //defaults to false + bidAdjustment: true, //defaults to true + enforceJS: true //defaults to true + }, + auctionDelay: 150, // in milliseconds defaults to 0 + endpoint: { + url: 'http://localhost:1500/floor-domains', + method: 'GET' // Only get supported for now + }, + data: { + schema: { + fields: ['mediaType', 'size'] + }, + values: { + 'banner|300x250': 0.86, + 'banner|300x600': 0.97, + 'banner|728x90': 1.12, + 'banner|*': 0.54, + 'video|640x480': 6.76, + 'video|1152x648': 11.76, + 'video|*': 4.55, + '*|*': 0.30 + }, + default: 0.01 + } + } +}); +``` + +| Parameter | Description | +|------------------------|---------------------------------------------------------------------------------------------------------------------| +| enabled | Wether to turn off or on the floors module | +| enforcement | object of booleans which control certain features of the module | +| auctionDelay | The time to suspend and auction while waiting for a real time price floors fetch to come back | +| endpoint | An object describing the endpoint to retrieve floor data from. GET only | +| data | The data to be used to select appropriate floors. See schema for more detail | +| additionalSchemaFields | An object of additional fields to be used in a floor data object. The schema is KEY: function to retrieve the match | + +### Passing floors to Bid Adapters +Because it is possible for many rules to match any given bidRequest, (wether it be due to more than one size or more than one mediaType), an encapsolated function is to be passed to bidders which will allow bidders to have insight as to what the floor could be. + +The function `getFloor` will be attached to every bidRequestObject passed to bid adapters if the price floors are enabled for a given auction. + +This function can takes in an object with the following optional parameters: + +| Parameter | Description | Example | Default | +|-----------|------------------------------------------------------------------------|------------|---------| +| currency | The 3 character currency code which the bid adapter wants the floor in | "JPY" | "USD" | +| mediaType | The specific mediaType to get a floor for | "banner" | "*" | +| size | The specific size to get a matching floor on | [300, 250] | "*" | + +If a bid adapter passes in `*` as an attribute, then the `priceFloors` module will attempt to select the best rule based on context. + +For example, if an adapter passes in a `*`, but the bidRequest only has a single size and a single mediaType, then the `getFloor` function will attempt to get a rule for that size before matching with the `*` catch-all. Similarily, if mediaType can be inferred on the bidRequest, it will use it. \ No newline at end of file diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 9bb8a924b98..80c881f479a 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -5,6 +5,7 @@ import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import * as utils from '../src/utils.js'; import * as urlLib from '../src/url.js' +import { getGlobal } from '../src/prebidGlobal.js'; const { EVENTS: { @@ -20,6 +21,9 @@ const { STATUS: { GOOD, NO_BID + }, + BID_STATUS: { + BID_REJECTED } } = CONSTANTS; @@ -101,7 +105,9 @@ function sendMessage(auctionId, bidWonId) { 'bidPriceUSD', 'dealId', 'dimensions', - 'mediaType' + 'mediaType', + 'floorValue', + 'floorRule' ]) : undefined ]); } @@ -143,9 +149,11 @@ function sendMessage(auctionId, bidWonId) { 'transactionId', 'mediaTypes', 'dimensions', - 'adserverTargeting', () => stringProperties(cache.targeting[bid.adUnit.adUnitCode] || {}) + 'adserverTargeting', () => stringProperties(cache.targeting[bid.adUnit.adUnitCode] || {}), + 'adSlot' ]); adUnit.bids = []; + adUnit.status = 'no-bid'; // default it to be no bid } // Add site and zone id if not there and if we found a rubicon bidder @@ -186,6 +194,17 @@ function sendMessage(auctionId, bidWonId) { adUnits: Object.keys(adUnitMap).map(i => adUnitMap[i]) }; + // pick our of top level floor data we want to send! + if (auctionCache.floorData) { + auction.floors = utils.pick(auctionCache.floorData, [ + 'location', + 'modelName', () => auctionCache.floorData.modelVersion || '', + 'skipped', + 'enforcement', () => utils.deepAccess(auctionCache.floorData, 'enforcements.enforceJS'), + 'dealsEnforced', () => utils.deepAccess(auctionCache.floorData, 'enforcements.floorDeals') + ]); + } + if (serverConfig) { auction.serverTimeoutMillis = serverConfig.timeout; } @@ -222,14 +241,31 @@ function sendMessage(auctionId, bidWonId) { } function getBidPrice(bid) { - if (typeof bid.currency === 'string' && bid.currency.toUpperCase() === 'USD') { + // get the cpm from bidResponse + let cpm; + let currency; + if (bid.status === BID_REJECTED && utils.deepAccess(bid, 'floorData.cpmAfterAdjustments')) { + // if bid was rejected and bid.floorData.cpmAfterAdjustments use it + cpm = bid.floorData.cpmAfterAdjustments; + currency = bid.floorData.floorCurrency; + } else if (typeof bid.currency === 'string' && bid.currency.toUpperCase() === 'USD') { + // bid is in USD use it return Number(bid.cpm); + } else { + // else grab cpm + cpm = bid.cpm; + currency = bid.currency; } - // use currency conversion function if present - if (typeof bid.getCpmInNewCurrency === 'function') { - return Number(bid.getCpmInNewCurrency('USD')); + // if after this it is still going and is USD then return it. + if (currency === 'USD') { + return Number(cpm); + } + // otherwise we convert and return + try { + return Number(getGlobal().convertCurrency(cpm, currency, 'USD')); + } catch (err) { + utils.logWarn('Rubicon Analytics Adapter: Could not determine the bidPriceUSD of the bid ', bid); } - utils.logWarn('Rubicon Analytics Adapter: Could not determine the bidPriceUSD of the bid ', bid); } export function parseBidResponse(bid, previousBidResponse) { @@ -249,6 +285,8 @@ export function parseBidResponse(bid, previousBidResponse) { 'height' ]), 'seatBidId', + 'floorValue', () => utils.deepAccess(bid, 'floorData.floorValue'), + 'floorRule', () => utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined ]); } @@ -329,6 +367,9 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.bids = {}; cacheEntry.bidsWon = {}; cacheEntry.referrer = args.bidderRequests[0].refererInfo.referer; + if (utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData')) { + cacheEntry.floorData = {...utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData')}; + } cache.auctions[args.auctionId] = cacheEntry; break; case BID_REQUESTED: @@ -399,14 +440,23 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { return Object.keys(types).filter(validMediaType); } return ['banner']; - } + }, ]) ]); return memo; }, {})); break; case BID_RESPONSE: - let bid = cache.auctions[args.auctionId].bids[args.requestId]; + let auctionEntry = cache.auctions[args.auctionId]; + let bid = auctionEntry.bids[args.requestId]; + // If floor resolved gptSlot but we have not yet, then update the adUnit to have the adSlot name + if (!utils.deepAccess(bid, 'adUnit.adSlot') && utils.deepAccess(args, 'floorData.matchedFields.gptSlot')) { + bid.adUnit.adSlot = args.floorData.matchedFields.gptSlot; + } + // if we have not set enforcements yet set it + if (!utils.deepAccess(auctionEntry, 'floorData.enforcements') && utils.deepAccess(args, 'floorData.enforcements')) { + auctionEntry.floorData.enforcements = {...args.floorData.enforcements}; + } if (!bid) { utils.logError('Rubicon Anlytics Adapter Error: Could not find associated bid request for bid response with requestId: ', args.requestId); break; @@ -418,7 +468,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { delete bid.error; // it's possible for this to be set by a previous timeout break; case NO_BID: - bid.status = 'no-bid'; + bid.status = args.status === BID_REJECTED ? 'rejected' : 'no-bid'; delete bid.error; break; default: diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index f5e83f0fe1e..f806dfdc468 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -192,7 +192,17 @@ export const spec = { } } - const bidFloor = parseFloat(utils.deepAccess(bidRequest, 'params.floor')); + let bidFloor; + if (typeof bidRequest.getFloor === 'function' && !config.getConfig('rubicon.disableFloors')) { + let floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'video', + size: parseSizes(bidRequest, 'video') + }); + bidFloor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; + } else { + bidFloor = parseFloat(utils.deepAccess(bidRequest, 'params.floor')); + } if (!isNaN(bidFloor)) { data.imp[0].bidfloor = bidFloor; } @@ -494,6 +504,16 @@ export const spec = { 'rf': _getPageUrl(bidRequest, bidderRequest) }; + // If floors module is enabled and we get USD floor back, send it in rp_hard_floor else undfined + if (typeof bidRequest.getFloor === 'function' && !config.getConfig('rubicon.disableFloors')) { + let floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'banner', + size: '*' + }); + data['rp_hard_floor'] = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; + } + // add p_pos only if specified and valid // For SRA we need to explicitly put empty semi colons so AE treats it as empty, instead of copying the latter value data['p_pos'] = (params.position === 'atf' || params.position === 'btf') ? params.position : ''; diff --git a/src/constants.json b/src/constants.json index 0b6b144a336..7ffef9db1aa 100644 --- a/src/constants.json +++ b/src/constants.json @@ -95,7 +95,8 @@ }, "BID_STATUS" : { "BID_TARGETING_SET": "targetingSet", - "RENDERED": "rendered" + "RENDERED": "rendered", + "BID_REJECTED": "bidRejected" }, "SUBMODULES_THAT_ALWAYS_REFRESH_ID": { "parrableId": true diff --git a/src/utils.js b/src/utils.js index fbc0e76d167..9b5b5b7bdac 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1078,6 +1078,24 @@ export function isSlotMatchingAdUnitCode(adUnitCode) { return (slot) => compareCodeAndSlot(slot, adUnitCode); } +/** + * @summary Uses the adUnit's code in order to find a matching gptSlot on the page + */ +export function getGptSlotInfoForAdUnitCode(adUnitCode) { + let matchingSlot; + if (isGptPubadsDefined()) { + // find the first matching gpt slot on the page + matchingSlot = find(window.googletag.pubads().getSlots(), isSlotMatchingAdUnitCode(adUnitCode)); + } + if (matchingSlot) { + return { + gptSlot: matchingSlot.getAdUnitPath(), + divId: matchingSlot.getSlotElementId() + } + } + return {}; +}; + /** * Constructs warning message for when unsupported bidders are dropped from an adunit * @param {Object} adUnit ad unit from which the bidder is being dropped diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index 577d36e9215..ccd205964a9 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -3,6 +3,8 @@ import { getCurrencyRates } from 'test/fixtures/fixtures.js'; +import { getGlobal } from 'src/prebidGlobal.js'; + import { setConfig, addBidResponseHook, @@ -43,8 +45,12 @@ describe('currency', function () { it('results in currencySupportEnabled = false when currency not configured', function () { setConfig({}); expect(currencySupportEnabled).to.equal(false); + expect(getGlobal().convertCurrency).to.be.undefined; + }); + it('adds conversion function onto pbjs global var once initiated', function () { + setConfig({ 'adServerCurrency': 'JPY' }); + expect(getGlobal().convertCurrency).to.be.a('function'); }); - it('results in currencySupportEnabled = true and currencyRates being loaded when configured', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); @@ -124,6 +130,38 @@ describe('currency', function () { }); }); + describe('global currency function', function () { + it('still returns conversion if default rates present with fetch not returned yet', function () { + setConfig({ + 'adServerCurrency': 'USD', + 'defaultRates': { + 'USD': { EUR: 2, JPY: 100 } + } + }); + // first conversion should use default rates + expect(getGlobal().convertCurrency(1.0, 'USD', 'EUR')).to.equal(2); + expect(getGlobal().convertCurrency(1.0, 'USD', 'JPY')).to.equal(100); + fakeCurrencyFileServer.respond(); + }); + it('uses fetch rates if returned', function () { + fakeCurrencyFileServer.respondWith(JSON.stringify({ + 'dataAsOf': '2017-04-25', + 'conversions': { + 'USD': { EUR: 4, JPY: 200 } + } + })); + setConfig({ + 'adServerCurrency': 'USD', + 'defaultRates': { + 'USD': { EUR: 2, JPY: 100 } + } + }); + // now respond and should use updates rates + fakeCurrencyFileServer.respond(); + expect(getGlobal().convertCurrency(1.0, 'USD', 'EUR')).to.equal(4); + expect(getGlobal().convertCurrency(1.0, 'USD', 'JPY')).to.equal(200); + }); + }); describe('bidder override', function () { it('allows setConfig to set bidder currency', function () { setConfig({}); diff --git a/test/spec/modules/priceFloorsSchema.json b/test/spec/modules/priceFloorsSchema.json new file mode 100644 index 00000000000..7b524da381e --- /dev/null +++ b/test/spec/modules/priceFloorsSchema.json @@ -0,0 +1,86 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "description": "A Price Floors object which is parsed and used to enforce dynamic floors depending on the properties of the json", + "type": "object", + "title": "Price Floors Enforcement", + "required": [ + "schema", + "values" + ], + "properties": { + "schema": { + "type": "object", + "description": "Defines the schema of the rules", + "required": [ + "fields" + ], + "properties": { + "fields": { + "type": "array", + "description": "The list of fields to be used for matching criteria of a bid response with a price floor", + "minItems": 1, + "uniqueItems": true, + "additionalItems": false, + "items": { + "type": "string" + } + }, + "delimiter": { + "type": "string", + "description": "The character used to differentiate the fields inside a single rule", + "examples": [ + "|", + "^", + "~" + ] + } + } + }, + "values": { + "type": "object", + "description": "A object with key : value pairs which constitutes a rule and floor", + "additionalProperties": { + "type": "number" + }, + "minProperties": 1, + "examples": [ + { + "123456/someSlot|300x250|www.prebid.org": 1.5, + "123456/someSlot|300x600|www.prebid.org": 2.5, + "123456/someSlot|300x600|*": 1.2, + "123456/someSlot|*|*": 0.8 + } + ] + }, + "currency": { + "type": "string", + "description": "The three digit Currency Code which the floors are provided in", + "examples": [ + "USD", + "EUR", + "JPY" + ], + "pattern": "^[a-zA-Z]{3}$" + }, + "modelVersion": { + "type": "string", + "description": "The floor modeling name to be used for tracking", + "examples": [ + "Prebid-Floor-Model-1.2" + ] + }, + "skipRate": { + "type": "integer", + "description": "The skip rate as to which flooring will be 'turned off' for a given auction", + "minimum": 0, + "maximum": 100 + }, + "default": { + "type": "number", + "description": "The default floor to use if no entry in the value matches a bid response", + "examples": [ + 0.75 + ] + } + } +} \ No newline at end of file diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js new file mode 100644 index 00000000000..55aa7900252 --- /dev/null +++ b/test/spec/modules/priceFloors_spec.js @@ -0,0 +1,972 @@ +import {expect} from 'chai'; +import * as utils from 'src/utils.js'; +import { getGlobal } from 'src/prebidGlobal.js'; +import CONSTANTS from 'src/constants.json'; +import { + _floorDataForAuction, + getFloorsDataForAuction, + getFirstMatchingFloor, + getFloor, + handleSetFloorsConfig, + requestBidsHook, + isFloorsDataValid, + addBidResponseHook, + fieldMatchingFunctions, + allowedFields +} from 'modules/priceFloors.js'; + +describe('the price floors module', function () { + let logErrorSpy; + let logWarnSpy; + let sandbox; + const basicFloorData = { + modelVersion: 'basic model', + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + 'video': 5.0, + '*': 2.5 + } + }; + const basicFloorConfig = { + enabled: true, + auctionDelay: 0, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: basicFloorData + } + const basicBidRequest = { + bidder: 'rubicon', + adUnitCode: 'test_div_1', + auctionId: '1234-56-789', + }; + + function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: {banner: { sizes: [[300, 200], [300, 600]] }, native: {}}, + bids: [{bidder: 'someBidder'}, {bidder: 'someOtherBidder'}] + }; + } + beforeEach(function() { + sandbox = sinon.sandbox.create(); + logErrorSpy = sinon.spy(utils, 'logError'); + logWarnSpy = sinon.spy(utils, 'logWarn'); + }); + + afterEach(function() { + handleSetFloorsConfig({enabled: false}); + sandbox.restore(); + utils.logError.restore(); + utils.logWarn.restore(); + // reset global bidder settings so no weird test side effects + getGlobal().bidderSettings = {}; + }); + + describe('getFloorsDataForAuction', function () { + it('converts basic input floor data into a floorData map for the auction correctly', function () { + // basic input where nothing needs to be updated + expect(getFloorsDataForAuction(basicFloorData)).to.deep.equal(basicFloorData); + + // if cur and delim not defined then default to correct ones (usd and |) + let inputFloorData = utils.deepClone(basicFloorData); + delete inputFloorData.currency; + delete inputFloorData.schema.delimiter; + expect(getFloorsDataForAuction(inputFloorData)).to.deep.equal(basicFloorData); + + // should not use defaults if differing values + inputFloorData.currency = 'EUR' + inputFloorData.schema.delimiter = '^' + let resultingData = getFloorsDataForAuction(inputFloorData); + expect(resultingData.currency).to.equal('EUR'); + expect(resultingData.schema.delimiter).to.equal('^'); + }); + + it('converts more complex floor data correctly', function () { + let inputFloorData = { + schema: { + fields: ['mediaType', 'size', 'domain'] + }, + values: { + 'banner|300x250|prebid.org': 1.0, + 'video|640x480|prebid.org': 5.0, + 'banner|728x90|rubicon.com': 3.5, + 'video|600x300|appnexus.com': 3.5, + '*|*|prebid.org': 3.5, + } + }; + let resultingData = getFloorsDataForAuction(inputFloorData); + expect(resultingData).to.deep.equal({ + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType', 'size', 'domain'] + }, + values: { + 'banner|300x250|prebid.org': 1.0, + 'video|640x480|prebid.org': 5.0, + 'banner|728x90|rubicon.com': 3.5, + 'video|600x300|appnexus.com': 3.5, + '*|*|prebid.org': 3.5, + } + }); + }); + + it('adds adUnitCode to the schema if the floorData comes from adUnit level to maintain scope', function () { + let inputFloorData = utils.deepClone(basicFloorData); + let resultingData = getFloorsDataForAuction(inputFloorData, 'test_div_1'); + expect(resultingData).to.deep.equal({ + modelVersion: 'basic model', + currency: 'USD', + schema: { + delimiter: '|', + fields: ['adUnitCode', 'mediaType'] + }, + values: { + 'test_div_1|banner': 1.0, + 'test_div_1|video': 5.0, + 'test_div_1|*': 2.5 + } + }); + + // uses the right delim if not | + inputFloorData.schema.delimiter = '^'; + resultingData = getFloorsDataForAuction(inputFloorData, 'this_is_a_div'); + expect(resultingData).to.deep.equal({ + modelVersion: 'basic model', + currency: 'USD', + schema: { + delimiter: '^', + fields: ['adUnitCode', 'mediaType'] + }, + values: { + 'this_is_a_div^banner': 1.0, + 'this_is_a_div^video': 5.0, + 'this_is_a_div^*': 2.5 + } + }); + }); + }); + + describe('getFirstMatchingFloor', function () { + it('selects the right floor for different mediaTypes', function () { + // banner with * size (not in rule file so does not do anything) + expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + matchingFloor: 1.0, + matchingData: 'banner', + matchingRule: 'banner' + }); + // video with * size (not in rule file so does not do anything) + expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ + matchingFloor: 5.0, + matchingData: 'video', + matchingRule: 'video' + }); + // native (not in the rule list) with * size (not in rule file so does not do anything) + expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'native', size: '*'})).to.deep.equal({ + matchingFloor: 2.5, + matchingData: 'native', + matchingRule: '*' + }); + }); + it('selects the right floor for different sizes', function () { + let inputFloorData = { + currency: 'USD', + schema: { + delimiter: '|', + fields: ['size'] + }, + values: { + '300x250': 1.1, + '640x480': 2.2, + '728x90': 3.3, + '600x300': 4.4, + '*': 5.5, + } + } + // banner with 300x250 size + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + matchingFloor: 1.1, + matchingData: '300x250', + matchingRule: '300x250' + }); + // video with 300x250 size + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + matchingFloor: 1.1, + matchingData: '300x250', + matchingRule: '300x250' + }); + // native (not in the rule list) with 300x600 size + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'native', size: [600, 300]})).to.deep.equal({ + matchingFloor: 4.4, + matchingData: '600x300', + matchingRule: '600x300' + }); + // n/a mediaType with a size not in file should go to catch all + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: undefined, size: [1, 1]})).to.deep.equal({ + matchingFloor: 5.5, + matchingData: '1x1', + matchingRule: '*' + }); + }); + it('selects the right floor for more complex rules', function () { + let inputFloorData = { + currency: 'USD', + schema: { + delimiter: '^', + fields: ['adUnitCode', 'mediaType', 'size'] + }, + values: { + 'test_div_1^banner^300x250': 1.1, + 'test_div_1^video^640x480': 2.2, + 'test_div_2^*^*': 3.3, + '*^banner^300x250': 4.4, + 'weird_div^*^300x250': 5.5 + }, + default: 0.5 + }; + // banner with 300x250 size + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + matchingFloor: 1.1, + matchingData: 'test_div_1^banner^300x250', + matchingRule: 'test_div_1^banner^300x250' + }); + // video with 300x250 size -> No matching rule so should use default + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + matchingFloor: 0.5, + matchingData: 'test_div_1^video^300x250', + matchingRule: undefined + }); + // remove default and should still return the same floor as above since matches are cached + delete inputFloorData.default; + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + matchingFloor: 0.5, + matchingData: 'test_div_1^video^300x250', + matchingRule: undefined + }); + // update adUnitCode to test_div_2 with weird other params + let newBidRequest = { ...basicBidRequest, adUnitCode: 'test_div_2' } + expect(getFirstMatchingFloor(inputFloorData, newBidRequest, {mediaType: 'badmediatype', size: [900, 900]})).to.deep.equal({ + matchingFloor: 3.3, + matchingData: 'test_div_2^badmediatype^900x900', + matchingRule: 'test_div_2^*^*' + }); + }); + it('it does not break if floorData has bad values', function () { + let inputFloorData = {}; + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + matchingFloor: undefined + }); + // if default is there use it + inputFloorData = { default: 5.0 }; + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + matchingFloor: 5.0 + }); + }); + }); + describe('pre-auction tests', function () { + let exposedAdUnits; + const validateBidRequests = (getFloorExpected, FloorDataExpected) => { + exposedAdUnits.forEach(adUnit => adUnit.bids.forEach(bid => { + expect(bid.hasOwnProperty('getFloor')).to.equal(getFloorExpected); + expect(bid.floorData).to.deep.equal(FloorDataExpected); + })); + }; + const runStandardAuction = (adUnits = [getAdUnitMock('test_div_1')]) => { + requestBidsHook(config => exposedAdUnits = config.adUnits, { + auctionId: basicBidRequest.auctionId, + adUnits, + }); + }; + let fakeFloorProvider; + let clock; + let actualAllowedFields = allowedFields; + let actualFieldMatchingFunctions = fieldMatchingFunctions; + const defaultAllowedFields = [...allowedFields]; + const defaultMatchingFunctions = {...fieldMatchingFunctions}; + before(function () { + clock = sinon.useFakeTimers(); + }); + after(function () { + clock.restore(); + }); + beforeEach(function() { + fakeFloorProvider = sinon.fakeServer.create(); + }); + afterEach(function() { + fakeFloorProvider.restore(); + exposedAdUnits = undefined; + actualAllowedFields = [...defaultAllowedFields]; + actualFieldMatchingFunctions = {...defaultMatchingFunctions}; + }); + it('should not do floor stuff if no resulting floor object can be resolved for auciton', function () { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + runStandardAuction(); + validateBidRequests(false, undefined); + }); + it('should use adUnit level data if not setConfig or fetch has occured', function () { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + // attach floor data onto an adUnit and run an auction + let adUnitWithFloors1 = { + ...getAdUnitMock('adUnit-Div-1'), + floors: { + ...basicFloorData, + modelVersion: 'adUnit Model Version', // change the model name + } + }; + let adUnitWithFloors2 = { + ...getAdUnitMock('adUnit-Div-2'), + floors: { + ...basicFloorData, + values: { + 'banner': 5.0, + '*': 10.4 + } + } + }; + runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]); + validateBidRequests(true, { + skipped: false, + modelVersion: 'adUnit Model Version', + location: 'adUnit', + }); + }); + it('bidRequests should have getFloor function and flooring meta data when setConfig occurs', function () { + handleSetFloorsConfig({...basicFloorConfig}); + runStandardAuction(); + validateBidRequests(true, { + skipped: false, + modelVersion: 'basic model', + location: 'setConfig', + }); + }); + it('should not overwrite previous data object if the new one is bad', function () { + handleSetFloorsConfig({...basicFloorConfig}); + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + handleSetFloorsConfig({ + ...basicFloorConfig, + data: 5 + }); + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + schema: {fields: ['thisIsNotAllowedSoShouldFail']}, + values: {'*': 1.2}, + modelVersion: 'FAIL' + } + }); + runStandardAuction(); + validateBidRequests(true, { + skipped: false, + modelVersion: 'basic model', + location: 'setConfig', + }); + }); + it('should dynamically add new schema fileds and functions if added via setConfig', function () { + let deviceSpoof; + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + schema: {fields: ['deviceType']}, + values: { + 'mobile': 1.0, + 'desktop': 2.0, + 'tablet': 3.0, + '*': 4.0 + } + }, + additionalSchemaFields: { + deviceType: () => deviceSpoof + } + }); + expect(allowedFields).to.contain('deviceType'); + expect(fieldMatchingFunctions['deviceType']).to.be.a('function'); + + // run getFloor to make sure it selcts right stuff! (other params do not matter since we only are testing deviceType) + runStandardAuction(); + + // set deviceType to mobile; + deviceSpoof = 'mobile'; + exposedAdUnits[0].bids[0].auctionId = basicBidRequest.auctionId + expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.0 // 'mobile': 1.0, + }); + + // set deviceType to desktop; + deviceSpoof = 'desktop'; + expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({ + currency: 'USD', + floor: 2.0 // 'desktop': 2.0, + }); + + // set deviceType to tablet; + deviceSpoof = 'tablet'; + expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({ + currency: 'USD', + floor: 3.0 // 'tablet': 3.0 + }); + + // set deviceType to unknown; + deviceSpoof = 'unknown'; + expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({ + currency: 'USD', + floor: 4.0 // '*': 4.0 + }); + }); + it('Should continue auction of delay is hit without a response from floor provider', function () { + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + + // start the auction it should delay and not immediately call `continueAuction` + runStandardAuction(); + + // exposedAdUnits should be undefined if the auction has not continued + expect(exposedAdUnits).to.be.undefined; + + // hit the delay + clock.tick(250); + + // log warn should be called and adUnits not undefined + expect(logWarnSpy.calledOnce).to.equal(true); + expect(exposedAdUnits).to.not.be.undefined; + + // the exposedAdUnits should be from the fetch not setConfig level data + validateBidRequests(true, { + skipped: false, + modelVersion: 'basic model', + location: 'setConfig', + }); + fakeFloorProvider.respond(); + }); + it('It should fetch if config has url and bidRequests have fetch level flooring meta data', function () { + // init the fake server with response stuff + let fetchFloorData = { + ...basicFloorData, + modelVersion: 'fetch model name', // change the model name + }; + fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + + // run setConfig indicating fetch + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + + // floor provider should be called + expect(fakeFloorProvider.requests.length).to.equal(1); + expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + + // start the auction it should delay and not immediately call `continueAuction` + runStandardAuction(); + + // exposedAdUnits should be undefined if the auction has not continued + expect(exposedAdUnits).to.be.undefined; + + // make the fetch respond + fakeFloorProvider.respond(); + expect(exposedAdUnits).to.not.be.undefined; + + // the exposedAdUnits should be from the fetch not setConfig level data + validateBidRequests(true, { + skipped: false, + modelVersion: 'fetch model name', + location: 'fetch', + }); + }); + it('Should not break if floor provider returns non json', function () { + fakeFloorProvider.respondWith('Not valid response'); + + // run setConfig indicating fetch + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + + // run the auction and make server respond + fakeFloorProvider.respond(); + runStandardAuction(); + + // should have caught the response error and still used setConfig data + validateBidRequests(true, { + skipped: false, + modelVersion: 'basic model', + location: 'setConfig', + }); + }); + it('should handle not using fetch correctly', function () { + // run setConfig twice indicating fetch + fakeFloorProvider.respondWith(JSON.stringify(basicFloorData)); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + + // log warn should be called and server only should have one request + expect(logWarnSpy.calledOnce).to.equal(true); + expect(fakeFloorProvider.requests.length).to.equal(1); + expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + + // now we respond and then run again it should work and make another request + fakeFloorProvider.respond(); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + fakeFloorProvider.respond(); + + // now warn still only called once and server called twice + expect(logWarnSpy.calledOnce).to.equal(true); + expect(fakeFloorProvider.requests.length).to.equal(2); + + // should log error if method is not GET for now + expect(logErrorSpy.calledOnce).to.equal(false); + handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakeFloorProvider.json', method: 'POST'}}); + expect(logErrorSpy.calledOnce).to.equal(true); + }); + describe('isFloorsDataValid', function () { + it('should work correctly for fields array', function () { + let inputFloorData = utils.deepClone(basicFloorData); + expect(isFloorsDataValid(inputFloorData)).to.to.equal(true); + + // no fields array + delete inputFloorData.schema.fields; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + + // Fields is not an array + inputFloorData.schema.fields = {}; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + inputFloorData.schema.fields = undefined; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + inputFloorData.schema.fields = 'adUnitCode'; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + + // fields has a value that is not one of the "allowed" fields + inputFloorData.schema.fields = ['adUnitCode', 'notValidMapping']; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + }); + it('should work correctly for values object', function () { + let inputFloorData = utils.deepClone(basicFloorData); + expect(isFloorsDataValid(inputFloorData)).to.to.equal(true); + + // no values object + delete inputFloorData.values; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + + // values is not correct type + inputFloorData.values = []; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + inputFloorData.values = '123455/slot'; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + + // is an object but structure is wrong + inputFloorData.values = { + 'banner': 'not a floor value' + }; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + inputFloorData.values = { + 'banner': undefined + }; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + + // should be true if at least one rule is valid + inputFloorData.schema.fields = ['adUnitCode', 'mediaType']; + inputFloorData.values = { + 'banner': 1.0, + 'test-div-1|native': 1.0, // only valid rule should still work and delete the other rules + 'video': 1.0, + '*': 1.0 + }; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(true); + expect(inputFloorData.values).to.deep.equal({ 'test-div-1|native': 1.0 }); + }); + }); + describe('getFloor', function () { + let bidRequest = { + ...basicBidRequest, + getFloor + }; + it('returns empty if no matching data for auction is found', function () { + expect(bidRequest.getFloor({})).to.deep.equal({}); + }); + it('picks the right rule depending on input', function () { + _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + + // empty params into getFloor should use default of banner * FloorData Curr + let inputParams = {}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 1.0 + }); + + // ask for banner + inputParams = {mediaType: 'banner'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 1.0 + }); + + // ask for video + inputParams = {mediaType: 'video'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 5.0 + }); + + // ask for * + inputParams = {mediaType: '*'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 2.5 + }); + }); + it('picks the right rule with more complex rules', function () { + _floorDataForAuction[bidRequest.auctionId] = { + ...basicFloorConfig, + data: { + currency: 'USD', + schema: { fields: ['mediaType', 'size'], delimiter: '|' }, + values: { + 'banner|300x250': 0.5, + 'banner|300x600': 1.5, + 'banner|728x90': 2.5, + 'banner|*': 3.5, + 'video|640x480': 4.5, + 'video|*': 5.5 + }, + default: 10.0 + } + }; + + // assumes banner * + let inputParams = {}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 3.5 + }); + + // ask for banner with a size + inputParams = {mediaType: 'banner', size: [300, 600]}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 1.5 + }); + + // ask for video with a size + inputParams = {mediaType: 'video', size: [640, 480]}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 4.5 + }); + + // ask for video with a size not in rules (should pick rule which has video and *) + inputParams = {mediaType: 'video', size: [111, 222]}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 5.5 + }); + + // ask for native * but no native rule so should use default value if there + inputParams = {mediaType: 'native', size: '*'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 10.0 + }); + }); + it('should round up to 4 decimal places', function () { + _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidRequest.auctionId].data.values = { + 'banner': 1.777777, + 'video': 1.1111111, + }; + + // assumes banner * + let inputParams = {mediaType: 'banner'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 1.7778 + }); + + // assumes banner * + inputParams = {mediaType: 'video'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 1.1112 + }); + }); + it('should return the adjusted floor if bidder has cpm adjustment function', function () { + getGlobal().bidderSettings = { + rubicon: { + bidCpmAdjustment: function (bidCpm) { + return bidCpm * 0.5; + }, + }, + appnexus: { + bidCpmAdjustment: function (bidCpm) { + return bidCpm * 0.75; + }, + } + }; + _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 }; + let appnexusBid = { + ...bidRequest, + bidder: 'appnexus' + }; + + // the conversion should be what the bidder would need to return in order to match the actual floor + // rubicon + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 2.0 // a 2.0 bid after rubicons cpm adjustment would be 1.0 and thus is the floor after adjust + }); + + // appnexus + expect(appnexusBid.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.3334 // 1.3334 * 0.75 = 1.000005 which is the floor (we cut off getFloor at 4 decimal points) + }); + }); + it('should correctly pick the right attributes if * is passed in and context can be assumed', function () { + let inputBidReq = { + bidder: 'rubicon', + adUnitCode: 'test_div_2', + auctionId: '987654321', + mediaTypes: { + video: {} + }, + getFloor + }; + _floorDataForAuction[inputBidReq.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[inputBidReq.auctionId].data.values = { + '*': 1.0, + 'banner': 3.0, + 'video': 5.0 + }; + + // because bid req only has video, if a bidder asks for a floor for * we can actually give them the right mediaType + expect(inputBidReq.getFloor({mediaType: '*'})).to.deep.equal({ + currency: 'USD', + floor: 5.0 // 'video': 5.0 + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // Same for if only banner is in the input bid + inputBidReq.mediaTypes = {banner: {}}; + expect(inputBidReq.getFloor({mediaType: '*'})).to.deep.equal({ + currency: 'USD', + floor: 3.0 // 'banner': 3.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // if both are present then it will really use the * + inputBidReq.mediaTypes = {banner: {}, video: {}}; + expect(inputBidReq.getFloor({mediaType: '*'})).to.deep.equal({ + currency: 'USD', + floor: 1.0 // '*': 1.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // now if size can be inferred (meaning only one size is in the specified mediaType, it will use it) + _floorDataForAuction[inputBidReq.auctionId].data.schema.fields = ['mediaType', 'size']; + _floorDataForAuction[inputBidReq.auctionId].data.values = { + '*|*': 1.0, + 'banner|300x250': 2.0, + 'banner|728x90': 3.0, + 'banner|*': 4.0, + 'video|300x250': 5.0, + 'video|728x90': 6.0, + 'video|*': 7.0 + }; + // mediaType is banner and only one size, so if someone asks for banner * we should give them banner 300x250 + // instead of banner|* + inputBidReq.mediaTypes = {banner: {sizes: [[300, 250]]}}; + expect(inputBidReq.getFloor({mediaType: 'banner', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 2.0 // 'banner|300x250': 2.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // now for video it should look at playersize (prebid core translates playersize into typical array of size arrays) + inputBidReq.mediaTypes = {video: {playerSize: [[728, 90]]}}; + expect(inputBidReq.getFloor({mediaType: 'video', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 6.0 // 'video|728x90': 6.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // Now if multiple sizes are there, it will actually use * since can't infer + inputBidReq.mediaTypes = {banner: {sizes: [[300, 250], [728, 90]]}}; + expect(inputBidReq.getFloor({mediaType: 'banner', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 4.0 // 'banner|*': 4.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // lastly, if you pass in * mediaType and * size it should resolve both if possble + inputBidReq.mediaTypes = {banner: {sizes: [[300, 250]]}}; + expect(inputBidReq.getFloor({mediaType: '*', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 2.0 // 'banner|300x250': 2.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + inputBidReq.mediaTypes = {video: {playerSize: [[300, 250]]}}; + expect(inputBidReq.getFloor({mediaType: '*', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 5.0 // 'video|300x250': 5.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // now it has both mediaTypes so will use * mediaType and thus not use sizes either + inputBidReq.mediaTypes = {video: {playerSize: [[300, 250]]}, banner: {sizes: [[300, 250]]}}; + expect(inputBidReq.getFloor({mediaType: '*', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 1.0 // '*|*': 1.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + }); + }); + }); + describe('bidResponseHook tests', function () { + let returnedBidResponse; + let bidderRequest = { + bidderCode: 'appnexus', + auctionId: '123456', + bids: [{ + bidder: 'appnexus', + adUnitCode: 'test_div_1', + auctionId: '123456', + bidId: '1111' + }] + }; + let basicBidResponse = { + bidderCode: 'appnexus', + width: 300, + height: 250, + cpm: 0.5, + mediaType: 'banner', + requestId: '1111', + }; + beforeEach(function () { + returnedBidResponse = {}; + }); + function runBidResponse(bidResp = basicBidResponse) { + let next = (adUnitCode, bid) => { + returnedBidResponse = bid; + }; + addBidResponseHook.bind({ bidderRequest })(next, bidResp.adUnitCode, bidResp); + }; + it('continues with the auction if not floors data is present without any flooring', function () { + runBidResponse(); + expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); + }); + it('if no matching rule it should not floor and should call log warn', function () { + _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidderRequest.auctionId].data.values = { 'video': 1.0 }; + runBidResponse(); + expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); + expect(logWarnSpy.calledOnce).to.equal(true); + }); + it('if it finds a rule and floors should update the bid accordingly', function () { + _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidderRequest.auctionId].data.values = { 'banner': 1.0 }; + runBidResponse(); + expect(returnedBidResponse).to.haveOwnProperty('floorData'); + expect(returnedBidResponse.status).to.equal(CONSTANTS.BID_STATUS.BID_REJECTED); + expect(returnedBidResponse.cpm).to.equal(0); + }); + it('if it finds a rule and does not floor should update the bid accordingly', function () { + _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidderRequest.auctionId].data.values = { 'banner': 0.3 }; + runBidResponse(); + expect(returnedBidResponse).to.haveOwnProperty('floorData'); + expect(returnedBidResponse.floorData).to.deep.equal({ + floorValue: 0.3, + floorCurrency: 'USD', + floorRule: 'banner', + cpmAfterAdjustments: 0.5, + enforcements: { + bidAdjustment: true, + enforceJS: true, + enforcePBS: false, + floorDeals: false + }, + matchedFields: { + mediaType: 'banner' + } + }); + expect(returnedBidResponse.cpm).to.equal(0.5); + }); + it('if should work with more complex rules and update accordingly', function () { + _floorDataForAuction[bidderRequest.auctionId] = { + ...basicFloorConfig, + data: { + currency: 'USD', + schema: { fields: ['mediaType', 'size'], delimiter: '|' }, + values: { + 'banner|300x250': 0.5, + 'banner|300x600': 1.5, + 'banner|728x90': 2.5, + 'banner|*': 3.5, + 'video|640x480': 4.5, + 'video|*': 5.5 + }, + default: 10.0 + } + }; + runBidResponse(); + expect(returnedBidResponse).to.haveOwnProperty('floorData'); + expect(returnedBidResponse.floorData).to.deep.equal({ + floorValue: 0.5, + floorCurrency: 'USD', + floorRule: 'banner|300x250', + cpmAfterAdjustments: 0.5, + enforcements: { + bidAdjustment: true, + enforceJS: true, + enforcePBS: false, + floorDeals: false + }, + matchedFields: { + mediaType: 'banner', + size: '300x250' + } + }); + expect(returnedBidResponse.cpm).to.equal(0.5); + + // update bidResponse to have different combinations (should pick video|*) + runBidResponse({ + width: 300, + height: 250, + cpm: 7.5, + mediaType: 'video', + requestId: '1111', + }); + expect(returnedBidResponse).to.haveOwnProperty('floorData'); + expect(returnedBidResponse.floorData).to.deep.equal({ + floorValue: 5.5, + floorCurrency: 'USD', + floorRule: 'video|*', + cpmAfterAdjustments: 7.5, + enforcements: { + bidAdjustment: true, + enforceJS: true, + enforcePBS: false, + floorDeals: false + }, + matchedFields: { + mediaType: 'video', + size: '300x250' + } + }); + expect(returnedBidResponse.cpm).to.equal(7.5); + }); + }); +}); diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 1842c29861d..9dc228ed288 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -659,6 +659,102 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(ANALYTICS_MESSAGE); }); + it('should capture price floor information correctly', function () { + let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); + auctionInit.bidderRequests[0].bids[0].floorData = { + skipped: false, + modelVersion: 'someModelName', + location: 'setConfig' + }; + 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' + } + } + }; + + // spoof the auction with just our duplicates + events.emit(AUCTION_INIT, auctionInit); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, flooredResponse); + events.emit(BID_RESPONSE, notFlooredResponse); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[1]); + clock.tick(SEND_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + + // verify our floor stuff is passed + // top level floor info + expect(message.auctions[0].floors).to.deep.equal({ + location: 'setConfig', + modelName: 'someModelName', + skipped: false, + enforcement: true, + dealsEnforced: false + }); + // first adUnit's adSlot + expect(message.auctions[0].adUnits[0].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'); + 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].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); + }); + it('should correctly overwrite bidId if seatBidId is on the bidResponse', function () { // Only want one bid request in our mock auction let bidRequested = utils.deepClone(MOCK.BID_REQUESTED); diff --git a/test/spec/modules/rubiconAnalyticsSchema.json b/test/spec/modules/rubiconAnalyticsSchema.json index 686aced840f..16cca629d8c 100644 --- a/test/spec/modules/rubiconAnalyticsSchema.json +++ b/test/spec/modules/rubiconAnalyticsSchema.json @@ -292,7 +292,8 @@ "enum": [ "success", "no-bid", - "error" + "error", + "rejected" ] }, "error": { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 7adedbcfdde..4e999f4bdab 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -15,7 +15,8 @@ const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be s describe('the rubicon adapter', function () { let sandbox, bidderRequest, - sizeMap; + sizeMap, + getFloorResponse; /** * @typedef {Object} sizeMapConverted @@ -274,7 +275,7 @@ describe('the rubicon adapter', function () { beforeEach(function () { sandbox = sinon.sandbox.create(); - + getFloorResponse = {}; bidderRequest = { bidderCode: 'rubicon', auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', @@ -411,6 +412,43 @@ describe('the rubicon adapter', function () { }); }); + it('should correctly send hard floors when getFloor function is present and returns valid floor', function () { + // default getFloor response is empty object so should not break and not send hard_floor + bidderRequest.bids[0].getFloor = () => getFloorResponse; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); + expect(data.rp_hard_floor).to.be.undefined; + + // not an object should work and not send + getFloorResponse = undefined; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + data = parseQuery(request.data); + expect(data.rp_hard_floor).to.be.undefined; + + // make it respond with a non USD floor should not send it + getFloorResponse = {currency: 'EUR', floor: 1.0}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + data = parseQuery(request.data); + expect(data.rp_hard_floor).to.be.undefined; + + // make it respond with a non USD floor should not send it + getFloorResponse = {currency: 'EUR'}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + data = parseQuery(request.data); + expect(data.rp_hard_floor).to.be.undefined; + + // make it respond with USD floor and string floor + getFloorResponse = {currency: 'USD', floor: '1.23'}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + data = parseQuery(request.data); + expect(data.rp_hard_floor).to.equal('1.23'); + + // make it respond with USD floor and num floor + getFloorResponse = {currency: 'USD', floor: 1.23}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + data = parseQuery(request.data); + expect(data.rp_hard_floor).to.equal('1.23'); + }); it('should not send p_pos to AE if not params.position specified', function() { var noposRequest = utils.deepClone(bidderRequest); delete noposRequest.bids[0].params.position; @@ -1450,6 +1488,39 @@ describe('the rubicon adapter', function () { expect(post.ext.prebid.cache.vastxml.returnCreative).to.equal(false) }); + it('should correctly set bidfloor on imp when getfloor in scope', function () { + createVideoBidderRequest(); + // default getFloor response is empty object so should not break and not send hard_floor + bidderRequest.bids[0].getFloor = () => getFloorResponse; + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); + + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // not an object should work and not send + expect(request.data.imp[0].bidfloor).to.be.undefined; + + // make it respond with a non USD floor should not send it + getFloorResponse = {currency: 'EUR', floor: 1.0}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.be.undefined; + + // make it respond with a non USD floor should not send it + getFloorResponse = {currency: 'EUR'}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.be.undefined; + + // make it respond with USD floor and string floor + getFloorResponse = {currency: 'USD', floor: '1.23'}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.equal(1.23); + + // make it respond with USD floor and num floor + getFloorResponse = {currency: 'USD', floor: 1.23}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.equal(1.23); + }); it('should add alias name to PBS Request', function() { createVideoBidderRequest(); From 25214abdd8504e63c3a5babbcc87768f45ccb4e5 Mon Sep 17 00:00:00 2001 From: Mirko Feddern Date: Tue, 31 Mar 2020 21:21:13 +0200 Subject: [PATCH 34/90] Add support for userid modules in Yieldlab adapter (#5022) * Add support for userid modules in Yieldlab adapter * Change for-of-loop for IE11 compatibility * Check if userIdAsEids is an array --- modules/yieldlabBidAdapter.js | 16 ++++++++++++++++ test/spec/modules/yieldlabBidAdapter_spec.js | 13 ++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index ca07449f12e..e1e491cdce9 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -39,6 +39,9 @@ export const spec = { if (bid.params.targeting) { query.t = createQueryString(bid.params.targeting) } + if (bid.userIdAsEids && Array.isArray(bid.userIdAsEids)) { + query.ids = createUserIdString(bid.userIdAsEids) + } }) if (bidderRequest && bidderRequest.gdprConsent) { @@ -162,6 +165,19 @@ function parseSize (size) { return size.split('x').map(Number) } +/** + * Creates a string out of an array of eids with source and uid + * @param {Array} eids + * @returns {String} + */ +function createUserIdString (eids) { + let str = [] + for (let i = 0; i < eids.length; i++) { + str.push(eids[i].source + ':' + eids[i].uids[0].id) + } + return str.join(',') +} + /** * Creates a querystring out of an object with key-values * @param {Object} obj diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index 584a9040bcf..5dcd112228a 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -18,7 +18,14 @@ const REQUEST = { 'auctionId': '2e41f65424c87c', 'adUnitCode': 'adunit-code', 'bidId': '2d925f27f5079f', - 'sizes': [728, 90] + 'sizes': [728, 90], + 'userIdAsEids': [{ + 'source': 'netid.de', + 'uids': [{ + 'id': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + 'atype': 1 + }] + }] } const RESPONSE = { @@ -77,6 +84,10 @@ describe('yieldlabBidAdapter', function () { expect(request.url).to.include('t=key1%3Dvalue1%26key2%3Dvalue2') }) + it('passes userids to bid request', function () { + expect(request.url).to.include('ids=netid.de%3AfH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg') + }) + const gdprRequest = spec.buildRequests(bidRequests, { gdprConsent: { consentString: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA', From ea3700a82034ce6939cca78ddbd64aac6b4e68ee Mon Sep 17 00:00:00 2001 From: Jaimin Panchal <7393273+jaiminpanchal27@users.noreply.github.com> Date: Tue, 31 Mar 2020 16:54:16 -0400 Subject: [PATCH 35/90] hide outstream div (#5055) Co-authored-by: Jaimin Panchal --- modules/appnexusBidAdapter.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index fc7ec77c267..c411bacd3ec 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -841,8 +841,20 @@ function buildNativeRequest(params) { return request; } +/** + * This function hides google div container for outstream bids to remove unwanted space on page. Appnexus renderer creates a new iframe outside of google iframe to render the outstream creative. + * @param {string} elementId element id + */ +function hidedfpContainer(elementId) { + var el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']"); + if (el[0]) { + el[0].style.setProperty('display', 'none'); + } +} + function outstreamRender(bid) { // push to render queue because ANOutstreamVideo may not be loaded yet + hidedfpContainer(bid.adUnitCode); bid.renderer.push(() => { window.ANOutstreamVideo.renderAd({ tagId: bid.adResponse.tag_id, From c1ccaf0e47323685a78efb3258fee11d22135080 Mon Sep 17 00:00:00 2001 From: tadam75 Date: Tue, 31 Mar 2020 22:55:45 +0200 Subject: [PATCH 36/90] Adding UsPrivacy/CCPA support in smartadserver adapter. (#5036) Co-authored-by: tadam75 --- modules/smartadserverBidAdapter.js | 4 +++ .../modules/smartadserverBidAdapter_spec.js | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index bddde2afd70..9f85ead5b9b 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -83,6 +83,10 @@ export const spec = { payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side } + if (bidderRequest && bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } + var payloadString = JSON.stringify(payload); return { method: 'POST', diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index 26855e4bb2e..65b603b9587 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -271,6 +271,35 @@ describe('Smart bid adapter tests', function () { }); }); + describe('ccpa/us privacy tests', function () { + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + }); + + it('Verify build request with us privacy', function () { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + }, + consentManagement: { + cmp: 'iab', + consentRequired: true, + timeout: 1000, + allowAuctionWithoutConsent: true + } + }); + + const uspConsentValue = '1YNN' + const request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, { + uspConsent: uspConsentValue + }); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent).to.have.property('us_privacy').and.to.equal(uspConsentValue); + }); + }); + describe('Instream video tests', function () { afterEach(function () { config.resetConfig(); From 3ee2c3db4629b7b12a741775cd97c91a6898d384 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal <7393273+jaiminpanchal27@users.noreply.github.com> Date: Tue, 31 Mar 2020 18:46:58 -0400 Subject: [PATCH 37/90] TCF Purpose 1 enforcement (#5018) * TCF Purpose 1 enforcement * some more unit tests and revert hello world page * some updates and unit test * remove unwanted comment * Adapter updates and some refactoring * add expires for ie11 * adding withCredentials false to appnexus adapter * improving conditions * improving conditions * Including changes for liveintent Co-authored-by: Jaimin Panchal --- integrationExamples/gpt/hello_world.html | 2 +- modules/aardvarkBidAdapter.js | 1 + modules/adagioBidAdapter.js | 11 +- modules/adkernelAdnAnalyticsAdapter.js | 6 +- modules/adspendBidAdapter.js | 8 +- modules/adtelligentBidAdapter.js | 1 + modules/aolBidAdapter.js | 1 + modules/appnexusBidAdapter.js | 28 +- modules/audigentRtdProvider.js | 7 +- modules/bidfluenceBidAdapter.js | 5 +- modules/browsiRtdProvider.js | 5 +- modules/categoryTranslation.js | 10 +- modules/ccxBidAdapter.js | 5 +- modules/cedatoBidAdapter.js | 5 +- modules/conversantBidAdapter.js | 11 +- modules/criteoBidAdapter.js | 10 +- modules/criteoIdSystem.js | 19 +- modules/digiTrustIdSystem.js | 7 +- modules/emoteevBidAdapter.js | 8 +- modules/eplanningBidAdapter.js | 15 +- modules/fidelityBidAdapter.js | 1 + modules/fintezaAnalyticsAdapter.js | 24 +- modules/gdprEnforcement.js | 218 +++++++++ modules/gumgumBidAdapter.js | 5 +- modules/id5IdSystem.js | 5 + modules/invibesBidAdapter.js | 14 +- modules/ixBidAdapter.js | 1 + modules/kargoBidAdapter.js | 7 +- modules/liveIntentIdSystem.js | 4 +- modules/livewrappedBidAdapter.js | 5 +- modules/mantisBidAdapter.js | 12 +- modules/medianetBidAdapter.js | 1 + modules/mgidBidAdapter.js | 7 +- modules/nanointeractiveBidAdapter.js | 9 +- modules/newborntownWebBidAdapter.js | 9 +- modules/nobidBidAdapter.js | 7 +- modules/oneVideoBidAdapter.js | 1 + modules/openxBidAdapter.js | 1 + modules/orbidderBidAdapter.js | 6 +- modules/pubCommonId.js | 31 +- modules/pubmaticBidAdapter.js | 1 + modules/pubwiseAnalyticsAdapter.js | 7 +- modules/pulsepointBidAdapter.js | 2 + modules/quantcastBidAdapter.js | 1 + modules/realvuAnalyticsAdapter.js | 13 +- modules/reloadBidAdapter.js | 8 +- modules/roxotAnalyticsAdapter.js | 15 +- modules/rubiconBidAdapter.js | 2 + modules/sigmoidAnalyticsAdapter.js | 19 +- modules/spotxBidAdapter.js | 1 + modules/staqAnalyticsAdapter.js | 7 +- modules/sublimeBidAdapter.js | 1 + modules/trionBidAdapter.js | 11 +- modules/unicornBidAdapter.js | 7 +- modules/userId/index.js | 40 +- modules/widespaceBidAdapter.js | 26 +- src/adapters/bidderFactory.js | 49 +- src/config.js | 5 + src/prebid.js | 15 + src/storageManager.js | 309 +++++++++++++ src/userSync.js | 5 +- src/utils.js | 124 ------ test/spec/modules/appnexusBidAdapter_spec.js | 27 ++ .../spec/modules/bidfluenceBidAdapter_spec.js | 38 +- test/spec/modules/categoryTranslation_spec.js | 4 +- .../spec/modules/conversantBidAdapter_spec.js | 22 +- test/spec/modules/criteoIdSystem_spec.js | 12 +- test/spec/modules/emoteevBidAdapter_spec.js | 17 +- test/spec/modules/eplanningBidAdapter_spec.js | 87 ++-- test/spec/modules/gdprEnforcement_spec.js | 418 ++++++++++++++++++ test/spec/modules/liveIntentIdSystem_spec.js | 6 +- .../modules/livewrappedBidAdapter_spec.js | 42 +- .../modules/newborntownWebBidAdapter_spec.js | 2 +- test/spec/modules/parrableIdSystem_spec.js | 7 +- test/spec/modules/userId_spec.js | 165 +++---- test/spec/modules/widespaceBidAdapter_spec.js | 13 +- test/spec/unit/core/bidderFactory_spec.js | 4 +- test/spec/unit/core/storageManager_spec.js | 46 ++ test/spec/utils_spec.js | 8 - 79 files changed, 1598 insertions(+), 521 deletions(-) create mode 100644 modules/gdprEnforcement.js create mode 100644 src/storageManager.js create mode 100644 test/spec/modules/gdprEnforcement_spec.js create mode 100644 test/spec/unit/core/storageManager_spec.js diff --git a/integrationExamples/gpt/hello_world.html b/integrationExamples/gpt/hello_world.html index d68e65011be..47ba5b8f18a 100755 --- a/integrationExamples/gpt/hello_world.html +++ b/integrationExamples/gpt/hello_world.html @@ -88,4 +88,4 @@
Div-1
- + \ No newline at end of file diff --git a/modules/aardvarkBidAdapter.js b/modules/aardvarkBidAdapter.js index 848a6672022..0b864286868 100644 --- a/modules/aardvarkBidAdapter.js +++ b/modules/aardvarkBidAdapter.js @@ -15,6 +15,7 @@ export function resetUserSync() { export const spec = { code: BIDDER_CODE, + gvlid: 52, aliases: ['adsparc', 'safereach'], isBidRequestValid: function(bid) { diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index d24350542a4..8b593c34270 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -4,6 +4,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { loadExternalScript } from '../src/adloader.js' import JSEncrypt from 'jsencrypt/bin/jsencrypt.js'; import sha256 from 'crypto-js/sha256.js'; +import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'adagio'; const VERSION = '2.1.0'; @@ -12,6 +13,8 @@ const ENDPOINT = 'https://mp.4dex.io/prebid'; const SUPPORTED_MEDIA_TYPES = ['banner']; const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js'; const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript'; +const GVLID = 617; +const storage = getStorageManager(GVLID); export const ADAGIO_PUBKEY = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9el0+OEn6fvEh1RdVHQu4cnT0 @@ -22,7 +25,7 @@ pV6EP3MTLosuUEpLaQIDAQAB export function getAdagioScript() { try { - const ls = utils.getDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); + const ls = storage.getDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); if (!ls) { utils.logWarn('Adagio Script not found'); @@ -33,7 +36,7 @@ export function getAdagioScript() { if (!hashRgx.test(ls)) { utils.logWarn('No hash found in Adagio script'); - utils.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); + storage.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); } else { const r = ls.match(hashRgx); const hash = r[2]; @@ -47,7 +50,7 @@ export function getAdagioScript() { Function(ls)(); // eslint-disable-line no-new-func } else { utils.logWarn('Invalid Adagio script found'); - utils.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); + storage.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); } } } catch (err) { @@ -341,7 +344,7 @@ function _getGdprConsent(bidderRequest) { export const spec = { code: BIDDER_CODE, - + gvlid: GVLID, supportedMediaType: SUPPORTED_MEDIA_TYPES, isBidRequestValid: function (bid) { diff --git a/modules/adkernelAdnAnalyticsAdapter.js b/modules/adkernelAdnAnalyticsAdapter.js index 43cda60b0ec..b0bdadf57d0 100644 --- a/modules/adkernelAdnAnalyticsAdapter.js +++ b/modules/adkernelAdnAnalyticsAdapter.js @@ -4,10 +4,12 @@ import adapterManager from '../src/adapterManager.js'; import {parse} from '../src/url.js'; import * as utils from '../src/utils.js'; import {ajax} from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; const ANALYTICS_VERSION = '1.0.1'; const DEFAULT_QUEUE_TIMEOUT = 4000; const DEFAULT_HOST = 'tag.adkernel.com'; +const storageObj = getStorageManager(); const ADK_HB_EVENTS = { AUCTION_INIT: 'auctionInit', @@ -175,10 +177,10 @@ const ORGANIC = '(organic)'; export let storage = { getItem: (name) => { - return utils.getDataFromLocalStorage(name); + return storageObj.getDataFromLocalStorage(name); }, setItem: (name, value) => { - utils.setDataInLocalStorage(name, value); + storageObj.setDataInLocalStorage(name, value); } }; diff --git a/modules/adspendBidAdapter.js b/modules/adspendBidAdapter.js index 3342a1934cc..9fe70885eeb 100644 --- a/modules/adspendBidAdapter.js +++ b/modules/adspendBidAdapter.js @@ -3,7 +3,9 @@ import { ajax } from '../src/ajax.js' import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; +const storage = getStorageManager(); const BIDDER_CODE = 'adspend'; const BID_URL = 'https://rtb.com.ru/headerbidding-bid'; const SYNC_URL = 'https://rtb.com.ru/headerbidding-sync?uid={UUID}'; @@ -40,7 +42,7 @@ export const spec = { bid.params.bidfloor && bid.crumbs.pubcid && utils.checkCookieSupport() && - utils.cookiesAreEnabled() + storage.cookiesAreEnabled() ); }, @@ -145,11 +147,11 @@ export const spec = { } const getUserID = () => { - const i = utils.getCookie(COOKIE_NAME); + const i = storage.getCookie(COOKIE_NAME); if (i === null) { const uuid = utils.generateUUID(); - utils.setCookie(COOKIE_NAME, uuid); + storage.setCookie(COOKIE_NAME, uuid); return uuid; } return i; diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index 5749866b65b..82601117017 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -13,6 +13,7 @@ const syncsCache = {}; export const spec = { code: BIDDER_CODE, + gvlid: 410, aliases: ['onefiftytwomedia', 'selectmedia'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { diff --git a/modules/aolBidAdapter.js b/modules/aolBidAdapter.js index d5fb2970628..d7ff7453870 100644 --- a/modules/aolBidAdapter.js +++ b/modules/aolBidAdapter.js @@ -100,6 +100,7 @@ function resolveEndpointCode(bid) { export const spec = { code: AOL_BIDDERS_CODES.AOL, + gvlid: 25, aliases: [AOL_BIDDERS_CODES.ONEMOBILE, AOL_BIDDERS_CODES.ONEDISPLAY], supportedMediaTypes: [BANNER], isBidRequestValid(bid) { diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index c411bacd3ec..f6a5e1d3eb9 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -7,6 +7,7 @@ import { auctionManager } from '../src/auctionManager.js'; import find from 'core-js/library/fn/array/find.js'; import includes from 'core-js/library/fn/array/includes.js'; import { OUTSTREAM, INSTREAM } from '../src/video.js'; +import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'appnexus'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -38,9 +39,12 @@ const mappingFileUrl = 'https://acdn.adnxs.com/prebid/appnexus-mapping/mappings. const SCRIPT_TAG_START = ' MAX_IMPS_PER_REQUEST) { const clonedPayload = utils.deepClone(payload); @@ -417,7 +437,8 @@ function formatRequest(payload, bidderRequest) { method: 'POST', url: URL, data: payloadString, - bidderRequest + bidderRequest, + options }); }); } else { @@ -426,7 +447,8 @@ function formatRequest(payload, bidderRequest) { method: 'POST', url: URL, data: payloadString, - bidderRequest + bidderRequest, + options }; } diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index 8b45287bddc..0f32c84962f 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -20,6 +20,9 @@ import {getGlobal} from '../src/prebidGlobal.js'; import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajax} from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -33,12 +36,12 @@ let _moduleParams = {}; */ export function setData(data) { - utils.setDataInLocalStorage('__adgntseg', JSON.stringify(data)); + storage.setDataInLocalStorage('__adgntseg', JSON.stringify(data)); } function getSegments(adUnits, onDone) { try { - let jsonData = utils.getDataFromLocalStorage('__adgntseg'); + let jsonData = storage.getDataFromLocalStorage('__adgntseg'); if (jsonData) { let data = JSON.parse(jsonData); if (data.audigent_segments) { diff --git a/modules/bidfluenceBidAdapter.js b/modules/bidfluenceBidAdapter.js index 6857a0f833f..f8a1f9ac92f 100644 --- a/modules/bidfluenceBidAdapter.js +++ b/modules/bidfluenceBidAdapter.js @@ -1,5 +1,8 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'bidfluence'; function stdTimezoneOffset(t) { @@ -46,7 +49,7 @@ export const spec = { var payload = { v: '2.0', azr: true, - ck: utils.cookiesAreEnabled(), + ck: storage.cookiesAreEnabled(), re: refInfo ? refInfo.referer : '', st: refInfo ? refInfo.stack : [], tz: getBdfTz(new Date()), diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index d3fe3176cda..3765b6603af 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -22,6 +22,9 @@ import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajaxBuilder} from '../src/ajax.js'; import {loadExternalScript} from '../src/adloader.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -62,7 +65,7 @@ function collectData() { const doc = win.document; let browsiData = null; try { - browsiData = utils.getDataFromLocalStorage('__brtd'); + browsiData = storage.getDataFromLocalStorage('__brtd'); } catch (e) { utils.logError('unable to parse __brtd'); } diff --git a/modules/categoryTranslation.js b/modules/categoryTranslation.js index 722fc59c921..5342220d13a 100644 --- a/modules/categoryTranslation.js +++ b/modules/categoryTranslation.js @@ -14,9 +14,11 @@ import { config } from '../src/config.js'; import { setupBeforeHookFnOnce, hook } from '../src/hook.js'; import { ajax } from '../src/ajax.js'; -import { timestamp, logError, setDataInLocalStorage, getDataFromLocalStorage } from '../src/utils.js'; +import { timestamp, logError } from '../src/utils.js'; import { addBidResponse } from '../src/auction.js'; +import { getCoreStorageManager } from '../src/storageManager.js'; +export const storage = getCoreStorageManager('categoryTranslation'); const DEFAULT_TRANSLATION_FILE_URL = 'https://cdn.jsdelivr.net/gh/prebid/category-mapping-file@1/freewheel-mapping.json'; const DEFAULT_IAB_TO_FW_MAPPING_KEY = 'iabToFwMappingkey'; const DEFAULT_IAB_TO_FW_MAPPING_KEY_PUB = 'iabToFwMappingkeyPub'; @@ -43,7 +45,7 @@ export function getAdserverCategoryHook(fn, adUnitCode, bid) { let localStorageKey = (config.getConfig('brandCategoryTranslation.translationFile')) ? DEFAULT_IAB_TO_FW_MAPPING_KEY_PUB : DEFAULT_IAB_TO_FW_MAPPING_KEY; if (bid.meta && !bid.meta.adServerCatId) { - let mapping = getDataFromLocalStorage(localStorageKey); + let mapping = storage.getDataFromLocalStorage(localStorageKey); if (mapping) { try { mapping = JSON.parse(mapping); @@ -65,7 +67,7 @@ export function getAdserverCategoryHook(fn, adUnitCode, bid) { export function initTranslation(url, localStorageKey) { setupBeforeHookFnOnce(addBidResponse, getAdserverCategoryHook, 50); - let mappingData = getDataFromLocalStorage(localStorageKey); + let mappingData = storage.getDataFromLocalStorage(localStorageKey); if (!mappingData || timestamp() < mappingData.lastUpdated + refreshInDays * 24 * 60 * 60 * 1000) { ajax(url, { @@ -73,7 +75,7 @@ export function initTranslation(url, localStorageKey) { try { response = JSON.parse(response); response['lastUpdated'] = timestamp(); - setDataInLocalStorage(localStorageKey, JSON.stringify(response)); + storage.setDataInLocalStorage(localStorageKey, JSON.stringify(response)); } catch (error) { logError('Failed to parse translation mapping file'); } diff --git a/modules/ccxBidAdapter.js b/modules/ccxBidAdapter.js index 5532ec6ca1e..ee15d6bb3ec 100644 --- a/modules/ccxBidAdapter.js +++ b/modules/ccxBidAdapter.js @@ -1,6 +1,9 @@ import * as utils from '../src/utils.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { config } from '../src/config.js' +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'ccx' const BID_URL = 'https://delivery.clickonometrics.pl/ortb/prebid/bid' const SUPPORTED_VIDEO_PROTOCOLS = [2, 3, 5, 6] @@ -170,7 +173,7 @@ export const spec = { requestBody.site = _getSiteObj(bidderRequest) requestBody.device = _getDeviceObj() requestBody.id = bidderRequest.bids[0].auctionId - requestBody.ext = {'ce': (utils.cookiesAreEnabled() ? 1 : 0)} + requestBody.ext = {'ce': (storage.cookiesAreEnabled() ? 1 : 0)} // Attaching GDPR Consent Params if (bidderRequest && bidderRequest.gdprConsent) { diff --git a/modules/cedatoBidAdapter.js b/modules/cedatoBidAdapter.js index 42a9baa514a..ab381698f01 100644 --- a/modules/cedatoBidAdapter.js +++ b/modules/cedatoBidAdapter.js @@ -1,6 +1,9 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'cedato'; const BID_URL = 'https://h.cedatoplayer.com/hb'; @@ -19,7 +22,7 @@ export const spec = { bid.params && bid.params.player_id && utils.checkCookieSupport() && - utils.cookiesAreEnabled() + storage.cookiesAreEnabled() ); }, diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index b1f3a1a0a06..25eed9c0e99 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -1,12 +1,17 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const GVLID = 24; +export const storage = getStorageManager(GVLID); const BIDDER_CODE = 'conversant'; const URL = 'https://web.hb.ad.cpe.dotomi.com/s2s/header/24'; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, aliases: ['cnvr'], // short code supportedMediaTypes: [BANNER, VIDEO], @@ -343,13 +348,13 @@ function readStoredValue(key) { let storedValue; try { // check cookies first - storedValue = utils.getCookie(key); + storedValue = storage.getCookie(key); if (!storedValue) { // check expiration time before reading local storage - const storedValueExp = utils.getDataFromLocalStorage(`${key}_exp`); + const storedValueExp = storage.getDataFromLocalStorage(`${key}_exp`); if (storedValueExp === '' || (storedValueExp && (new Date(storedValueExp)).getTime() - Date.now() > 0)) { - storedValue = utils.getDataFromLocalStorage(key); + storedValue = storage.getDataFromLocalStorage(key); storedValue = storedValue ? decodeURIComponent(storedValue) : storedValue; } } diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index caebb12c717..65675248208 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -6,13 +6,16 @@ import { parse } from '../src/url.js'; import * as utils from '../src/utils.js'; import find from 'core-js/library/fn/array/find.js'; import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; +import { getStorageManager } from '../src/storageManager.js'; +const GVLID = 91; export const ADAPTER_VERSION = 26; const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; const CRITEO_VENDOR_ID = 91; const PROFILE_ID_INLINE = 207; export const PROFILE_ID_PUBLISHERTAG = 185; +const storage = getStorageManager(GVLID); // Unminified source code can be found in: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js const PUBLISHER_TAG_URL = 'https://static.criteo.net/js/ld/publishertag.prebid.js'; @@ -23,6 +26,7 @@ const FAST_BID_PUBKEY_N = 'ztQYwCE5BU7T9CDM5he6rKoabstXRmkzx54zFPZkWbK530dwtLBDe /** @type {BidderSpec} */ export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [ BANNER, VIDEO, NATIVE ], /** @@ -423,7 +427,7 @@ export function tryGetCriteoFastBid() { try { const fastBidStorageKey = 'criteo_fast_bid'; const hashPrefix = '// Hash: '; - const fastBidFromStorage = utils.getDataFromLocalStorage(fastBidStorageKey); + const fastBidFromStorage = storage.getDataFromLocalStorage(fastBidStorageKey); if (fastBidFromStorage !== null) { // The value stored must contain the file's encrypted hash as first line @@ -432,7 +436,7 @@ export function tryGetCriteoFastBid() { if (firstLine.substr(0, hashPrefix.length) !== hashPrefix) { utils.logWarn('No hash found in FastBid'); - utils.removeDataFromLocalStorage(fastBidStorageKey); + storage.removeDataFromLocalStorage(fastBidStorageKey); } else { // Remove the hash part from the locally stored value const publisherTagHash = firstLine.substr(hashPrefix.length); @@ -446,7 +450,7 @@ export function tryGetCriteoFastBid() { utils.insertElement(script); } else { utils.logWarn('Invalid Criteo FastBid found'); - utils.removeDataFromLocalStorage(fastBidStorageKey); + storage.removeDataFromLocalStorage(fastBidStorageKey); } } } diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index 469532e4aa7..0103db8ef80 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -10,6 +10,9 @@ import * as ajax from '../src/ajax.js' import * as urlLib from '../src/url.js' import { getRefererInfo } from '../src/refererDetection.js' import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); const bididStorageKey = 'cto_bidid'; const bundleStorageKey = 'cto_bundle'; @@ -20,9 +23,9 @@ const pastDateString = new Date(0).toString(); const expirationString = new Date(utils.timestamp() + cookiesMaxAge).toString(); function areCookiesWriteable() { - utils.setCookie(cookieWriteableKey, '1'); - const canWrite = utils.getCookie(cookieWriteableKey) === '1'; - utils.setCookie(cookieWriteableKey, '', pastDateString); + storage.setCookie(cookieWriteableKey, '1'); + const canWrite = storage.getCookie(cookieWriteableKey) === '1'; + storage.setCookie(cookieWriteableKey, '', pastDateString); return canWrite; } @@ -34,19 +37,19 @@ function extractProtocolHost (url, returnOnlyHost = false) { } function getFromAllStorages(key) { - return utils.getCookie(key) || utils.getDataFromLocalStorage(key); + return storage.getCookie(key) || storage.getDataFromLocalStorage(key); } function saveOnAllStorages(key, value) { if (key && value) { - utils.setCookie(key, value, expirationString); - utils.setDataInLocalStorage(key, value); + storage.setCookie(key, value, expirationString); + storage.setDataInLocalStorage(key, value); } } function deleteFromAllStorages(key) { - utils.setCookie(key, '', pastDateString); - utils.removeDataFromLocalStorage(key); + storage.setCookie(key, '', pastDateString); + storage.removeDataFromLocalStorage(key); } function getCriteoDataFromAllStorages() { diff --git a/modules/digiTrustIdSystem.js b/modules/digiTrustIdSystem.js index eeb1a4f3b60..0b964c43d57 100644 --- a/modules/digiTrustIdSystem.js +++ b/modules/digiTrustIdSystem.js @@ -12,6 +12,10 @@ import * as utils from '../src/utils.js' import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const DT_VENDOR_ID = 64; // cmp gvlVendorId +const storage = getStorageManager(DT_VENDOR_ID); var fallbackTimeout = 1550; // timeout value that allows userId system to execute first var fallbackTimer = 0; // timer Id for fallback init so we don't double call @@ -40,7 +44,6 @@ var noop = function () { const MAX_RETRIES = 2; const DT_ID_SVC = 'https://prebid.digitru.st/id/v1'; -const DT_VENDOR_ID = 64; // cmp gvlVendorId var isFunc = function (fn) { return typeof (fn) === 'function'; @@ -88,7 +91,7 @@ function writeDigiId(id) { var key = 'DigiTrust.v1.identity'; var date = new Date(); date.setTime(date.getTime() + 604800000); - utils.setCookie(key, encId(id), date.toUTCString(), 'none'); + storage.setCookie(key, encId(id), date.toUTCString(), 'none'); } /** diff --git a/modules/emoteevBidAdapter.js b/modules/emoteevBidAdapter.js index 254373b354a..0bdf4cda58e 100644 --- a/modules/emoteevBidAdapter.js +++ b/modules/emoteevBidAdapter.js @@ -23,11 +23,13 @@ import { deepAccess, isArray, isInteger, - getParameterByName, - getCookie + getParameterByName } from '../src/utils.js'; import {config} from '../src/config.js'; import * as url from '../src/url.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); export const BIDDER_CODE = 'emoteev'; @@ -508,7 +510,7 @@ export const spec = { onBidWon: (bidObject) => triggerPixel(url.format(onBidWon( resolveEnv(config.getConfig(), getParameterByName('emoteevEnv')), - getCookie('_pubcid'), + storage.getCookie('_pubcid'), bidObject))), onTimeout: (bidRequest) => triggerPixel(url.format(onTimeout( diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index 979f6dbf464..a4b4f2d6728 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -1,5 +1,8 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); const BIDDER_CODE = 'eplanning'; const rnd = Math.random(); @@ -43,7 +46,7 @@ export const spec = { url = 'https://' + (urlConfig.sv || DEFAULT_SV) + '/hb/1/' + urlConfig.ci + '/' + dfpClientId + '/' + getDomain(pageUrl) + '/' + sec; const referrerUrl = bidderRequest.refererInfo.referer.reachedTop ? encodeURIComponent(window.top.document.referrer) : encodeURIComponent(bidderRequest.refererInfo.referer); - if (utils.hasLocalStorage()) { + if (storage.hasLocalStorage()) { registerViewabilityAllBids(bidRequests); } @@ -204,7 +207,7 @@ function getSpaces(bidRequests, ml) { function getVs(bid) { let s; let vs = ''; - if (utils.hasLocalStorage()) { + if (storage.hasLocalStorage()) { s = getViewabilityData(bid); vs += s.render >= 4 ? s.ratio.toString(16) : 'F'; } else { @@ -214,8 +217,8 @@ function getVs(bid) { } function getViewabilityData(bid) { - let r = utils.getDataFromLocalStorage(STORAGE_RENDER_PREFIX + bid.adUnitCode) || 0; - let v = utils.getDataFromLocalStorage(STORAGE_VIEW_PREFIX + bid.adUnitCode) || 0; + let r = storage.getDataFromLocalStorage(STORAGE_RENDER_PREFIX + bid.adUnitCode) || 0; + let v = storage.getDataFromLocalStorage(STORAGE_VIEW_PREFIX + bid.adUnitCode) || 0; let ratio = r > 0 ? (v / r) : 0; return { render: r, @@ -408,9 +411,9 @@ function visibilityHandler(obj) { function registerAuction(storageID) { let value; try { - value = utils.getDataFromLocalStorage(storageID); + value = storage.getDataFromLocalStorage(storageID); value = value ? window.parseInt(value, 10) + 1 : 1; - utils.setDataInLocalStorage(storageID, value); + storage.setDataInLocalStorage(storageID, value); } catch (exc) { return false; } diff --git a/modules/fidelityBidAdapter.js b/modules/fidelityBidAdapter.js index a0589986af3..7bca0745cc5 100644 --- a/modules/fidelityBidAdapter.js +++ b/modules/fidelityBidAdapter.js @@ -6,6 +6,7 @@ const BIDDER_SERVER = 'x.fidelity-media.com'; const FIDELITY_VENDOR_ID = 408; export const spec = { code: BIDDER_CODE, + gvlid: 408, isBidRequestValid: function isBidRequestValid(bid) { return !!(bid && bid.params && bid.params.zoneid); }, diff --git a/modules/fintezaAnalyticsAdapter.js b/modules/fintezaAnalyticsAdapter.js index 41343cdf371..f454e6b3acb 100644 --- a/modules/fintezaAnalyticsAdapter.js +++ b/modules/fintezaAnalyticsAdapter.js @@ -3,7 +3,9 @@ import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; import { parse as parseURL } from '../src/url.js'; +import { getStorageManager } from '../src/storageManager.js'; +const storage = getStorageManager(); const CONSTANTS = require('../src/constants.json'); const ANALYTICS_TYPE = 'endpoint'; @@ -46,8 +48,8 @@ function getUniqId() { let uniq = cookies[ UNIQ_ID_KEY ]; if (!uniq) { try { - if (utils.hasLocalStorage()) { - uniq = utils.getDataFromLocalStorage(UNIQ_ID_KEY) || ''; + if (storage.hasLocalStorage()) { + uniq = storage.getDataFromLocalStorage(UNIQ_ID_KEY) || ''; isUniqFromLS = true; } } catch (b) {} @@ -62,7 +64,7 @@ function getUniqId() { expires.setFullYear(expires.getFullYear() + 10); try { - utils.setCookie(UNIQ_ID_KEY, uniq, expires.toUTCString()); + storage.setCookie(UNIQ_ID_KEY, uniq, expires.toUTCString()); } catch (e) {} } @@ -90,7 +92,7 @@ function initFirstVisit() { now.setFullYear(now.getFullYear() + 20); try { - utils.setCookie(FIRST_VISIT_DATE, visitDate, now.toUTCString()); + storage.setCookie(FIRST_VISIT_DATE, visitDate, now.toUTCString()); } catch (e) {} } @@ -110,7 +112,7 @@ function parseCookies(cookie) { let param, value; let i, j; - if (!cookie || !utils.cookiesAreEnabled()) { + if (!cookie || !storage.cookiesAreEnabled()) { return {}; } @@ -203,7 +205,7 @@ function initSession() { } try { - utils.setCookie(SESSION_ID, sessionId, expires.toUTCString()); + storage.setCookie(SESSION_ID, sessionId, expires.toUTCString()); } catch (e) {} return { @@ -249,10 +251,10 @@ function saveTrackRequestTime() { const expires = new Date(now + SESSION_DURATION); try { - if (utils.hasLocalStorage()) { - utils.setDataInLocalStorage(TRACK_TIME_KEY, now.toString()); + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage(TRACK_TIME_KEY, now.toString()); } else { - utils.setCookie(TRACK_TIME_KEY, now.toString(), expires.toUTCString()); + storage.setCookie(TRACK_TIME_KEY, now.toString(), expires.toUTCString()); } } catch (a) {} } @@ -261,9 +263,9 @@ function getTrackRequestLastTime() { let cookie; try { - if (utils.hasLocalStorage()) { + if (storage.hasLocalStorage()) { return parseInt( - utils.getDataFromLocalStorage(TRACK_TIME_KEY) || 0, + storage.getDataFromLocalStorage(TRACK_TIME_KEY) || 0, 10, ); } diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js new file mode 100644 index 00000000000..363d0e396f8 --- /dev/null +++ b/modules/gdprEnforcement.js @@ -0,0 +1,218 @@ +/** + * This module gives publishers extra set of features to enforce individual purposes of TCF v2 + */ + +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { hasDeviceAccess } from '../src/utils.js'; +import adapterManager, { gdprDataHandler } from '../src/adapterManager.js'; +import find from 'core-js/library/fn/array/find.js'; +import includes from 'core-js/library/fn/array/includes.js'; +import { registerSyncInner } from '../src/adapters/bidderFactory.js'; +import { getHook } from '../src/hook.js'; +import { validateStorageEnforcement } from '../src/storageManager.js'; + +const purpose1 = 'storage'; + +let addedDeviceAccessHook = false; +let enforcementRules; + +function getGvlid() { + let gvlid; + const bidderCode = config.getCurrentBidder(); + if (bidderCode) { + const bidder = adapterManager.getBidAdapter(bidderCode); + gvlid = bidder.getSpec().gvlid; + } else { + utils.logWarn('Current module not found'); + } + return gvlid; +} + +/** + * This function takes in rules and consentData as input and validates against the consentData provided. If it returns true Prebid will allow the next call else it will log a warning + * @param {Object} rules enforcement rules set in config + * @param {Object} consentData gdpr consent data + * @returns {boolean} + */ +function validateRules(rule, consentData, currentModule, gvlid) { + let isAllowed = false; + if (!rule.vendorExceptions) rule.vendorExceptions = []; + if (rule.enforcePurpose && rule.enforceVendor) { + if ( + includes(rule.vendorExceptions, currentModule) || + ( + utils.deepAccess(consentData, 'vendorData.purpose.consents.1') === true && + utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true + ) + ) { + isAllowed = true; + } + } else if (rule.enforcePurpose === false && rule.enforceVendor === true) { + if ( + includes(rule.vendorExceptions, currentModule) || + ( + utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true + ) + ) { + isAllowed = true; + } + } else if (rule.enforcePurpose === false && rule.enforceVendor === false) { + if ( + !includes(rule.vendorExceptions, currentModule) || + ( + (utils.deepAccess(consentData, 'vendorData.purpose.consents.1') === true) && + (utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true) + ) + ) { + isAllowed = true; + } + } else if (rule.enforcePurpose === true && rule.enforceVendor === false) { + if ( + (utils.deepAccess(consentData, 'vendorData.purpose.consents.1') === true) && + ( + !includes(rule.vendorExceptions, currentModule) || + (utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true) + ) + ) { + isAllowed = true; + } + } + return isAllowed; +} + +/** + * This hook checks whether module has permission to access device or not. Device access include cookie and local storage + * @param {Function} fn reference to original function (used by hook logic) + * @param {Number=} gvlid gvlid of the module + * @param {string=} moduleName name of the module + */ +export function deviceAccessHook(fn, gvlid, moduleName, result) { + result = Object.assign({}, { + hasEnforcementHook: true + }); + if (!hasDeviceAccess()) { + utils.logWarn('Device access is disabled by Publisher'); + result.valid = false; + fn.call(this, gvlid, moduleName, result); + } else { + const consentData = gdprDataHandler.getConsentData(); + if (consentData && consentData.gdprApplies) { + if (consentData.apiVersion === 2) { + if (!gvlid) { + gvlid = getGvlid(); + } + const curModule = moduleName || config.getCurrentBidder(); + const purpose1Rule = find(enforcementRules, hasPurpose1); + let isAllowed = validateRules(purpose1Rule, consentData, curModule, gvlid); + if (isAllowed) { + result.valid = true; + fn.call(this, gvlid, moduleName, result); + } else { + utils.logWarn(`User denied Permission for Device access for ${curModule}`); + result.valid = false; + fn.call(this, gvlid, moduleName, result); + } + } else { + utils.logInfo('Enforcing TCF2 only'); + result.valid = true; + fn.call(this, gvlid, moduleName, result); + } + } else { + result.valid = true; + fn.call(this, gvlid, moduleName, result); + } + } +} + +/** + * This hook checks if a bidder has consent for user sync or not + * @param {Function} fn reference to original function (used by hook logic) + * @param {...any} args args + */ +export function userSyncHook(fn, ...args) { + const consentData = gdprDataHandler.getConsentData(); + if (consentData && consentData.gdprApplies) { + if (consentData.apiVersion === 2) { + const gvlid = getGvlid(); + const curBidder = config.getCurrentBidder(); + if (gvlid) { + const purpose1Rule = find(enforcementRules, hasPurpose1); + let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); + if (isAllowed) { + fn.call(this, ...args); + } else { + utils.logWarn(`User sync not allowed for ${curBidder}`); + } + } else { + utils.logWarn(`User sync not allowed for ${curBidder}`); + } + } else { + utils.logInfo('Enforcing TCF2 only'); + fn.call(this, ...args); + } + } else { + fn.call(this, ...args); + } +} + +/** + * This hook checks if user id module is given consent or not + * @param {Function} fn reference to original function (used by hook logic) + * @param {Submodule[]} submodules Array of user id submodules + * @param {Object} consentData GDPR consent data + */ +export function userIdHook(fn, submodules, consentData) { + if (consentData && consentData.gdprApplies) { + if (consentData.apiVersion === 2) { + let userIdModules = submodules.map((submodule) => { + const gvlid = submodule.submodule.gvlid; + const moduleName = submodule.submodule.name; + if (gvlid) { + const purpose1Rule = find(enforcementRules, hasPurpose1); + let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); + if (isAllowed) { + return submodule; + } else { + utils.logWarn(`User denied permission to fetch user id for ${moduleName} User id module`); + } + } else { + utils.logWarn(`User denied permission to fetch user id for ${moduleName} User id module`); + } + return undefined; + }).filter(module => module) + fn.call(this, userIdModules, consentData); + } else { + utils.logInfo('Enforcing TCF2 only'); + fn.call(this, submodules, consentData); + } + } else { + fn.call(this, submodules, consentData); + } +} + +const hasPurpose1 = (rule) => { return rule.purpose === purpose1 } + +/** + * A configuration function that initializes some module variables, as well as add hooks + * @param {Object} config GDPR enforcement config object + */ +export function setEnforcementConfig(config) { + const rules = utils.deepAccess(config, 'gdpr.rules'); + if (!rules) { + utils.logWarn('GDPR enforcement rules not defined, exiting enforcement module'); + return; + } + + enforcementRules = rules; + const hasDefinedPurpose1 = find(enforcementRules, hasPurpose1); + if (hasDefinedPurpose1 && !addedDeviceAccessHook) { + addedDeviceAccessHook = true; + validateStorageEnforcement.before(deviceAccessHook, 49); + registerSyncInner.before(userSyncHook, 48); + // Using getHook as user id and gdprEnforcement are both optional modules. Using import will auto include the file in build + getHook('validateGdprEnforcement').before(userIdHook, 47); + } +} + +config.getConfig('consentManagement', config => setEnforcementConfig(config.consentManagement)); diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index baa79ecfe04..3a347163e6d 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -4,6 +4,9 @@ import { config } from '../src/config.js' import { BANNER, VIDEO } from '../src/mediaTypes.js'; import includes from 'core-js/library/fn/array/includes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'gumgum' const ALIAS_BIDDER_CODE = ['gg'] @@ -55,7 +58,7 @@ function _getBrowserParams(topWindowUrl) { sw: topScreen.width, sh: topScreen.height, pu: topUrl, - ce: utils.cookiesAreEnabled(), + ce: storage.cookiesAreEnabled(), dpr: topWindow.devicePixelRatio || 1, jcsi: JSON.stringify({ t: 0, rq: 8 }), ogu: getOgURL() diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 32617d2f328..1d55450239b 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -16,6 +16,11 @@ export const id5IdSubmodule = { * @type {string} */ name: 'id5Id', + /** + * Vendor id of ID5 + * @type {Number} + */ + gvlid: 131, /** * decode the stored id value for passing to bid requests * @function decode diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index 95737c15c3d..e839c173a93 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -1,5 +1,7 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; +const storage = getStorageManager(); const CONSTANTS = { BIDDER_CODE: 'invibes', @@ -276,14 +278,14 @@ function getCappedCampaignsAsString() { let loadData = function () { try { - return JSON.parse(utils.getDataFromLocalStorage(key)) || {}; + return JSON.parse(storage.getDataFromLocalStorage(key)) || {}; } catch (e) { return {}; } }; let saveData = function (data) { - utils.setDataInLocalStorage(key, JSON.stringify(data)); + storage.setDataInLocalStorage(key, JSON.stringify(data)); }; let clearExpired = function () { @@ -319,7 +321,7 @@ function getCappedCampaignsAsString() { const noop = function () { }; function initLogger() { - if (utils.hasLocalStorage() && localStorage.InvibesDEBUG) { + if (storage.hasLocalStorage() && localStorage.InvibesDEBUG) { return window.console; } @@ -384,7 +386,7 @@ invibes.Uid = { let cookieDomain; invibes.getCookie = function (name) { - if (!utils.cookiesAreEnabled()) { return; } + if (!storage.cookiesAreEnabled()) { return; } let i, x, y; let cookies = document.cookie.split(';'); for (i = 0; i < cookies.length; i++) { @@ -398,7 +400,7 @@ invibes.getCookie = function (name) { }; invibes.setCookie = function (name, value, exdays, domain) { - if (!utils.cookiesAreEnabled()) { return; } + if (!storage.cookiesAreEnabled()) { return; } let whiteListed = name == 'ivNoCookie' || name == 'IvbsCampIdsLocal'; if (invibes.noCookies && !whiteListed && (exdays || 0) >= 0) { return; } if (exdays > 365) { exdays = 365; } @@ -406,7 +408,7 @@ invibes.setCookie = function (name, value, exdays, domain) { let exdate = new Date(); let exms = exdays * 24 * 60 * 60 * 1000; exdate.setTime(exdate.getTime() + exms); - utils.setCookie(name, value, exdate.toUTCString(), undefined, domain); + storage.setCookie(name, value, exdate.toUTCString(), undefined, domain); }; let detectTopmostCookieDomain = function () { diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 98896db9337..2725bafdff8 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -329,6 +329,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { export const spec = { code: BIDDER_CODE, + gvlid: 10, supportedMediaTypes: SUPPORTED_AD_TYPES, /** diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 6d86038e6bd..31c35f4afe3 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -1,6 +1,9 @@ import * as utils from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'kargo'; const HOST = 'https://krk.kargo.com'; const SYNC = 'https://crb.kargo.com/api/v1/initsyncrnd/{UUID}?seed={SEED}&idx={INDEX}'; @@ -98,7 +101,7 @@ export const spec = { // PRIVATE _readCookie(name) { - if (!utils.cookiesAreEnabled()) { + if (!storage.cookiesAreEnabled()) { return null; } let nameEquals = `${name}=`; @@ -173,7 +176,7 @@ export const spec = { _getLocalStorageSafely(key) { try { - return utils.getDataFromLocalStorage(key); + return storage.getDataFromLocalStorage(key); } catch (e) { return null; } diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index c020e80d2c6..3f5d0602166 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -9,8 +9,10 @@ import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { LiveConnect } from 'live-connect-js/cjs/live-connect.js'; import { uspDataHandler } from '../src/adapterManager.js'; +import { getStorageManager } from '../src/storageManager.js'; const MODULE_NAME = 'liveIntentId'; +export const storage = getStorageManager(null, MODULE_NAME); let eventFired = false; let liveConnect = null; @@ -83,7 +85,7 @@ function initializeLiveConnect(configParams) { } // The second param is the storage object, which means that all LS & Cookie manipulation will go through PBJS utils. - liveConnect = LiveConnect(liveConnectConfig, utils); + liveConnect = LiveConnect(liveConnectConfig, storage); return liveConnect; } diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js index 0e478869b53..5a9ef302d51 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -3,6 +3,9 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import find from 'core-js/library/fn/array/find.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); const BIDDER_CODE = 'livewrapped'; export const URL = 'https://lwadm.com/ad'; @@ -71,7 +74,7 @@ export const spec = { version: VERSION, gdprApplies: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.gdprApplies : undefined, gdprConsent: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.consentString : undefined, - cookieSupport: !utils.isSafariBrowser() && utils.cookiesAreEnabled(), + cookieSupport: !utils.isSafariBrowser() && storage.cookiesAreEnabled(), rcv: getAdblockerRecovered(), adRequests: [...adRequests], rtbData: handleEids(bidRequests) diff --git a/modules/mantisBidAdapter.js b/modules/mantisBidAdapter.js index 4e32f3f70ac..19e70a3e68b 100644 --- a/modules/mantisBidAdapter.js +++ b/modules/mantisBidAdapter.js @@ -1,5 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); function inIframe() { try { @@ -93,9 +95,9 @@ function storeUuid(uuid) { return false; } window.mantis_uuid = uuid; - if (utils.hasLocalStorage()) { + if (storage.hasLocalStorage()) { try { - utils.setDataInLocalStorage('mantis:uuid', uuid); + storage.setDataInLocalStorage('mantis:uuid', uuid); } catch (ex) { } } @@ -176,8 +178,8 @@ function buildMantisUrl(path, data, domain) { } if (window.mantis_uuid) { params.uuid = window.mantis_uuid; - } else if (utils.hasLocalStorage()) { - var localUuid = utils.getDataFromLocalStorage('mantis:uuid'); + } else if (storage.hasLocalStorage()) { + var localUuid = storage.getDataFromLocalStorage('mantis:uuid'); if (localUuid) { params.uuid = localUuid; } diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index f68f263a0d5..d944f354c83 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -297,6 +297,7 @@ function clearMnData() { export const spec = { code: BIDDER_CODE, + gvlid: 142, supportedMediaTypes: [BANNER, NATIVE], diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index f423f5d474b..194a8255641 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -3,6 +3,9 @@ import * as utils from '../src/utils.js'; import * as urlUtils from '../src/url.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const DEFAULT_CUR = 'USD'; const BIDDER_CODE = 'mgid'; const ENDPOINT_URL = 'https://prebid.mgid.com/prebid/'; @@ -342,7 +345,7 @@ function getLanguage() { function getLocalStorageSafely(key) { try { - return utils.getDataFromLocalStorage(key); + return storage.getDataFromLocalStorage(key); } catch (e) { return null; } @@ -350,7 +353,7 @@ function getLocalStorageSafely(key) { function setLocalStorageSafely(key, val) { try { - return utils.setDataInLocalStorage(key, val); + return storage.setDataInLocalStorage(key, val); } catch (e) { return null; } diff --git a/modules/nanointeractiveBidAdapter.js b/modules/nanointeractiveBidAdapter.js index 3dfd4811bcd..42a343efc03 100644 --- a/modules/nanointeractiveBidAdapter.js +++ b/modules/nanointeractiveBidAdapter.js @@ -1,6 +1,9 @@ import * as utils from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); export const BIDDER_CODE = 'nanointeractive'; export const END_POINT_URL = 'https://ad.audiencemanager.de'; @@ -97,7 +100,7 @@ function createSingleBidRequest(bid, bidderRequest) { function createSingleBidResponse(serverBid) { if (serverBid.userId) { - utils.setDataInLocalStorage('lsUserId', serverBid.userId); + storage.setDataInLocalStorage('lsUserId', serverBid.userId); } return { requestId: serverBid.id, @@ -147,8 +150,8 @@ function getEndpointUrl() { } function getLsUserId() { - if (utils.getDataFromLocalStorage('lsUserId') != null) { - return utils.getDataFromLocalStorage('lsUserId'); + if (storage.getDataFromLocalStorage('lsUserId') != null) { + return storage.getDataFromLocalStorage('lsUserId'); } return null; } diff --git a/modules/newborntownWebBidAdapter.js b/modules/newborntownWebBidAdapter.js index 0ad3e212089..56c63e2bb4d 100644 --- a/modules/newborntownWebBidAdapter.js +++ b/modules/newborntownWebBidAdapter.js @@ -1,6 +1,9 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'newborntownWeb'; const REQUEST_URL = 'https://us-west.solortb.com/adx/api/rtb?from=4' @@ -53,10 +56,10 @@ export const spec = { return null; } var guid; - if (utils.getDataFromLocalStorage('sax_user_id') == null) { - utils.setDataInLocalStorage('sax_user_id', generateGUID()) + if (storage.getDataFromLocalStorage('sax_user_id') == null) { + storage.setDataInLocalStorage('sax_user_id', generateGUID()) } - guid = utils.getDataFromLocalStorage('sax_user_id') + guid = storage.getDataFromLocalStorage('sax_user_id') utils._each(validBidRequests, function(bidRequest) { const bidRequestObj = bidRequest.params var req = { diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index a845693425d..dead2d48ed4 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -2,6 +2,9 @@ import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'nobid'; window.nobidVersion = '1.2.4'; window.nobid = window.nobid || {}; @@ -16,10 +19,10 @@ function nobidSetCookie(cname, cvalue, hours) { var d = new Date(); d.setTime(d.getTime() + (hours * 60 * 60 * 1000)); var expires = 'expires=' + d.toUTCString(); - utils.setCookie(cname, cvalue, expires); + storage.setCookie(cname, cvalue, expires); } function nobidGetCookie(cname) { - return utils.getCookie(cname); + return storage.getCookie(cname); } function nobidBuildRequests(bids, bidderRequest) { var serializeState = function(divIds, siteId, adunits) { diff --git a/modules/oneVideoBidAdapter.js b/modules/oneVideoBidAdapter.js index 9d3fabcd3b6..933f2d7a4c8 100644 --- a/modules/oneVideoBidAdapter.js +++ b/modules/oneVideoBidAdapter.js @@ -3,6 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'oneVideo'; export const spec = { code: 'oneVideo', + gvlid: 25, VERSION: '3.0.0', ENDPOINT: 'https://ads.adaptv.advertising.com/rtb/openrtb?ext_id=', SYNC_ENDPOINT1: 'https://cm.g.doubleclick.net/pixel?google_nid=adaptv_dbm&google_cm&google_sc', diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 8638dbd6d5d..a469a71ecb2 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -24,6 +24,7 @@ export const USER_ID_CODE_TO_QUERY_ARG = { export const spec = { code: BIDDER_CODE, + gvlid: 69, supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function (bidRequest) { const hasDelDomainOrPlatform = bidRequest.params.delDomain || bidRequest.params.platform; diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index 3461aaf66a4..d7ce5aa859a 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -1,7 +1,9 @@ import {detectReferer} from '../src/refererDetection.js'; import {ajax} from '../src/ajax.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); export const spec = { code: 'orbidder', @@ -9,7 +11,7 @@ export const spec = { orbidderHost: (() => { let ret = 'https://orbidder.otto.de'; try { - ret = utils.getDataFromLocalStorage('ov_orbidder_host') || ret; + ret = storage.getDataFromLocalStorage('ov_orbidder_host') || ret; } catch (e) { } return ret; diff --git a/modules/pubCommonId.js b/modules/pubCommonId.js index f476091ce2b..d69ce0f2086 100644 --- a/modules/pubCommonId.js +++ b/modules/pubCommonId.js @@ -8,6 +8,9 @@ import { config } from '../src/config.js'; import events from '../src/events.js'; import * as url from '../src/url.js'; import CONSTANTS from '../src/constants.json'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const ID_NAME = '_pubcid'; const OPTOUT_NAME = '_pubcid_optout'; @@ -37,10 +40,10 @@ export function setStorageItem(key, val, expires) { try { if (expires !== undefined && expires != null) { const expStr = (new Date(Date.now() + (expires * 60 * 1000))).toUTCString(); - utils.setDataInLocalStorage(key + EXP_SUFFIX, expStr); + storage.setDataInLocalStorage(key + EXP_SUFFIX, expStr); } - utils.setDataInLocalStorage(key, val); + storage.setDataInLocalStorage(key, val); } catch (e) { utils.logMessage(e); } @@ -55,18 +58,18 @@ export function getStorageItem(key) { let val = null; try { - const expVal = utils.getDataFromLocalStorage(key + EXP_SUFFIX); + const expVal = storage.getDataFromLocalStorage(key + EXP_SUFFIX); if (!expVal) { // If there is no expiry time, then just return the item - val = utils.getDataFromLocalStorage(key); + val = storage.getDataFromLocalStorage(key); } else { // Only return the item if it hasn't expired yet. // Otherwise delete the item. const expDate = new Date(expVal); const isValid = (expDate.getTime() - Date.now()) > 0; if (isValid) { - val = utils.getDataFromLocalStorage(key); + val = storage.getDataFromLocalStorage(key); } else { removeStorageItem(key); } @@ -84,8 +87,8 @@ export function getStorageItem(key) { */ export function removeStorageItem(key) { try { - utils.removeDataFromLocalStorage(key + EXP_SUFFIX); - utils.removeDataFromLocalStorage(key); + storage.removeDataFromLocalStorage(key + EXP_SUFFIX); + storage.removeDataFromLocalStorage(key); } catch (e) { utils.logMessage(e); } @@ -101,7 +104,7 @@ function readValue(name, type) { let value; if (!type) { type = pubcidConfig.typeEnabled; } if (type === COOKIE) { - value = utils.getCookie(name); + value = storage.getCookie(name); } else if (type === LOCAL_STORAGE) { value = getStorageItem(name); } @@ -223,12 +226,12 @@ export function requestBidHook(next, config) { export function setCookie(name, value, expires, sameSite) { let expTime = new Date(); expTime.setTime(expTime.getTime() + expires * 1000 * 60); - utils.setCookie(name, value, expTime.toGMTString(), sameSite); + storage.setCookie(name, value, expTime.toGMTString(), sameSite); } // Helper to read a cookie export function getCookie(name) { - return utils.getCookie(name); + return storage.getCookie(name); } /** @@ -263,12 +266,12 @@ export function setConfig({ enable, expInterval, type = 'html5,cookie', create, for (let i = 0; i < typeArray.length; ++i) { const name = typeArray[i].trim(); if (name === COOKIE) { - if (utils.cookiesAreEnabled()) { + if (storage.cookiesAreEnabled()) { pubcidConfig.typeEnabled = COOKIE; break; } } else if (name === LOCAL_STORAGE) { - if (utils.hasLocalStorage()) { + if (storage.hasLocalStorage()) { pubcidConfig.typeEnabled = LOCAL_STORAGE; break; } @@ -282,8 +285,8 @@ export function setConfig({ enable, expInterval, type = 'html5,cookie', create, export function initPubcid() { config.getConfig('pubcid', config => setConfig(config.pubcid)); - const optout = (utils.cookiesAreEnabled() && readValue(OPTOUT_NAME, COOKIE)) || - (utils.hasLocalStorage() && readValue(OPTOUT_NAME, LOCAL_STORAGE)); + const optout = (storage.cookiesAreEnabled() && readValue(OPTOUT_NAME, COOKIE)) || + (storage.hasLocalStorage() && readValue(OPTOUT_NAME, LOCAL_STORAGE)); if (!optout) { $$PREBID_GLOBAL$$.requestBids.before(requestBidHook); diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 02c43e1c2a9..605ee69603f 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -817,6 +817,7 @@ function _handleDealCustomTargetings(payload, dctrArr, validBidRequests) { export const spec = { code: BIDDER_CODE, + gvlid: 76, supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid diff --git a/modules/pubwiseAnalyticsAdapter.js b/modules/pubwiseAnalyticsAdapter.js index df38b3ab076..915aeb58f99 100644 --- a/modules/pubwiseAnalyticsAdapter.js +++ b/modules/pubwiseAnalyticsAdapter.js @@ -2,8 +2,11 @@ import {ajax} from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; +import { getStorageManager } from '../src/storageManager.js'; const utils = require('../src/utils.js'); +const storage = getStorageManager(); + /**** * PubWise.io Analytics * Contact: support@pubwise.io @@ -59,14 +62,14 @@ function enrichWithUTM(dataBag) { if (newUtm === false) { for (let prop in utmKeys) { - let itemValue = utils.getDataFromLocalStorage(`pw-${prop}`); + let itemValue = storage.getDataFromLocalStorage(`pw-${prop}`); if (itemValue.length !== 0) { dataBag[prop] = itemValue; } } } else { for (let prop in utmKeys) { - utils.setDataInLocalStorage(`pw-${prop}`, utmKeys[prop]); + storage.setDataInLocalStorage(`pw-${prop}`, utmKeys[prop]); } } } catch (e) { diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index d3655906cf0..7bfa8686728 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -28,6 +28,8 @@ export const spec = { code: 'pulsepoint', + gvlid: 81, + aliases: ['pulseLite', 'pulsepointLite'], supportedMediaTypes: ['banner', 'native', 'video'], diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index 4ffb84b8934..ab7ea54bfb6 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -78,6 +78,7 @@ function getDomain(url) { */ export const spec = { code: BIDDER_CODE, + GVLID: 11, supportedMediaTypes: ['banner', 'video'], /** diff --git a/modules/realvuAnalyticsAdapter.js b/modules/realvuAnalyticsAdapter.js index 4b05a998788..95e62397c2c 100644 --- a/modules/realvuAnalyticsAdapter.js +++ b/modules/realvuAnalyticsAdapter.js @@ -2,6 +2,9 @@ import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const utils = require('../src/utils.js'); @@ -779,7 +782,7 @@ export let lib = { writePos: function (a) { try { let v = a.x + ',' + a.y + ',' + a.w + ',' + a.h; - utils.setDataInLocalStorage(this.keyPos(a), v); + storage.setDataInLocalStorage(this.keyPos(a), v); } catch (ex) { /* continue regardless of error */ } @@ -787,7 +790,7 @@ export let lib = { readPos: function (a) { try { - let s = utils.getDataFromLocalStorage(this.keyPos(a)); + let s = storage.getDataFromLocalStorage(this.keyPos(a)); if (s) { let v = s.split(','); a.x = parseInt(v[0], 10); @@ -806,7 +809,7 @@ export let lib = { incrMem: function(a, evt, name) { try { let k1 = this.keyPos(a) + '.' + name; - let vmem = utils.getDataFromLocalStorage(k1); + let vmem = storage.getDataFromLocalStorage(k1); if (vmem == null) vmem = '1:3'; let vr = vmem.split(':'); let nv = parseInt(vr[0], 10); @@ -819,7 +822,7 @@ export let lib = { if (evt == 'v') { nv |= 1; } - utils.setDataInLocalStorage(k1, nv + ':' + nr); + storage.setDataInLocalStorage(k1, nv + ':' + nr); } catch (ex) { /* do nothing */ } @@ -827,7 +830,7 @@ export let lib = { score: function (a, name) { try { - let vstr = utils.getDataFromLocalStorage(this.keyPos(a) + '.' + name); + let vstr = storage.getDataFromLocalStorage(this.keyPos(a) + '.' + name); if (vstr != null) { let vr = vstr.split(':'); let nv = parseInt(vr[0], 10); diff --git a/modules/reloadBidAdapter.js b/modules/reloadBidAdapter.js index dd4514c99c8..94ea4be281f 100644 --- a/modules/reloadBidAdapter.js +++ b/modules/reloadBidAdapter.js @@ -3,6 +3,10 @@ import { } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); + const BIDDER_CODE = 'reload'; const VERSION_ADAPTER = '1.10'; export const spec = { @@ -390,14 +394,14 @@ function ReloadClientTool(args) { var stgFileStr = JSON.stringify(stgFileObj); - utils.setDataInLocalStorage(name, stgFileStr); + storage.setDataInLocalStorage(name, stgFileStr); return true; } function _getItem (name) { try { - var obStgFileStr = utils.getDataFromLocalStorage(name); + var obStgFileStr = storage.getDataFromLocalStorage(name); if (obStgFileStr === null) return null; var stgFileObj = JSON.parse(obStgFileStr); diff --git a/modules/roxotAnalyticsAdapter.js b/modules/roxotAnalyticsAdapter.js index a077c7ac7e0..0e9cda9b6bc 100644 --- a/modules/roxotAnalyticsAdapter.js +++ b/modules/roxotAnalyticsAdapter.js @@ -3,6 +3,9 @@ import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; import includes from 'core-js/library/fn/array/includes.js'; import {ajaxBuilder} from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const utils = require('../src/utils.js'); let ajax = ajaxBuilder(0); @@ -68,17 +71,17 @@ function detectDevice() { function checkIsNewFlag() { let key = buildLocalStorageKey(isNewKey); - let lastUpdate = Number(utils.getDataFromLocalStorage(key)); - utils.setDataInLocalStorage(key, Date.now()); + let lastUpdate = Number(storage.getDataFromLocalStorage(key)); + storage.setDataInLocalStorage(key, Date.now()); return Date.now() - lastUpdate > isNewTtl; } function updateUtmTimeout() { - utils.setDataInLocalStorage(buildLocalStorageKey(utmTtlKey), Date.now()); + storage.setDataInLocalStorage(buildLocalStorageKey(utmTtlKey), Date.now()); } function isUtmTimeoutExpired() { - let utmTimestamp = utils.getDataFromLocalStorage(buildLocalStorageKey(utmTtlKey)); + let utmTimestamp = storage.getDataFromLocalStorage(buildLocalStorageKey(utmTtlKey)); return (Date.now() - utmTimestamp) > utmTtl; } @@ -356,11 +359,11 @@ roxotAdapter.buildUtmTagData = function () { }); utmTags.forEach(function (utmTagKey) { if (utmTagsDetected) { - utils.setDataInLocalStorage(buildLocalStorageKey(utmTagKey), utmTagData[utmTagKey]); + storage.setDataInLocalStorage(buildLocalStorageKey(utmTagKey), utmTagData[utmTagKey]); updateUtmTimeout(); } else { if (!isUtmTimeoutExpired()) { - utmTagData[utmTagKey] = utils.getDataFromLocalStorage(buildLocalStorageKey(utmTagKey)) ? utils.getDataFromLocalStorage(buildLocalStorageKey(utmTagKey)) : ''; + utmTagData[utmTagKey] = storage.getDataFromLocalStorage(buildLocalStorageKey(utmTagKey)) ? storage.getDataFromLocalStorage(buildLocalStorageKey(utmTagKey)) : ''; updateUtmTimeout(); } } diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index f806dfdc468..616a17f470f 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -10,6 +10,7 @@ export const FASTLANE_ENDPOINT = 'https://fastlane.rubiconproject.com/a/api/fast export const VIDEO_ENDPOINT = 'https://prebid-server.rubiconproject.com/openrtb2/auction'; export const SYNC_ENDPOINT = 'https://eus.rubiconproject.com/usync.html'; +const GVLID = 52; const DIGITRUST_PROP_NAMES = { FASTLANE: { id: 'dt.id', @@ -113,6 +114,7 @@ utils._each(sizeMap, (item, key) => sizeMap[item] = key); export const spec = { code: 'rubicon', + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], /** * @param {object} bid diff --git a/modules/sigmoidAnalyticsAdapter.js b/modules/sigmoidAnalyticsAdapter.js index fac23730864..88bb5eb23a9 100644 --- a/modules/sigmoidAnalyticsAdapter.js +++ b/modules/sigmoidAnalyticsAdapter.js @@ -4,6 +4,9 @@ import includes from 'core-js/library/fn/array/includes.js'; import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const utils = require('../src/utils.js'); @@ -54,31 +57,31 @@ function buildSessionIdTimeoutLocalStorageKey() { function updateSessionId() { if (isSessionIdTimeoutExpired()) { let newSessionId = utils.generateUUID(); - utils.setDataInLocalStorage(buildSessionIdLocalStorageKey(), newSessionId); + storage.setDataInLocalStorage(buildSessionIdLocalStorageKey(), newSessionId); } initOptions.sessionId = getSessionId(); updateSessionIdTimeout(); } function updateSessionIdTimeout() { - utils.setDataInLocalStorage(buildSessionIdTimeoutLocalStorageKey(), Date.now()); + storage.setDataInLocalStorage(buildSessionIdTimeoutLocalStorageKey(), Date.now()); } function isSessionIdTimeoutExpired() { - let cpmSessionTimestamp = utils.getDataFromLocalStorage(buildSessionIdTimeoutLocalStorageKey()); + let cpmSessionTimestamp = storage.getDataFromLocalStorage(buildSessionIdTimeoutLocalStorageKey()); return Date.now() - cpmSessionTimestamp > sessionTimeout; } function getSessionId() { - return utils.getDataFromLocalStorage(buildSessionIdLocalStorageKey()) ? utils.getDataFromLocalStorage(buildSessionIdLocalStorageKey()) : ''; + return storage.getDataFromLocalStorage(buildSessionIdLocalStorageKey()) ? storage.getDataFromLocalStorage(buildSessionIdLocalStorageKey()) : ''; } function updateUtmTimeout() { - utils.setDataInLocalStorage(buildUtmLocalStorageTimeoutKey(), Date.now()); + storage.setDataInLocalStorage(buildUtmLocalStorageTimeoutKey(), Date.now()); } function isUtmTimeoutExpired() { - let utmTimestamp = utils.getDataFromLocalStorage(buildUtmLocalStorageTimeoutKey()); + let utmTimestamp = storage.getDataFromLocalStorage(buildUtmLocalStorageTimeoutKey()); return (Date.now() - utmTimestamp) > utmTimeout; } @@ -219,11 +222,11 @@ sigmoidAdapter.buildUtmTagData = function () { }); utmTags.forEach(function(utmTagKey) { if (utmTagsDetected) { - utils.setDataInLocalStorage(buildUtmLocalStorageKey(utmTagKey), utmTagData[utmTagKey]); + storage.setDataInLocalStorage(buildUtmLocalStorageKey(utmTagKey), utmTagData[utmTagKey]); updateUtmTimeout(); } else { if (!isUtmTimeoutExpired()) { - utmTagData[utmTagKey] = utils.getDataFromLocalStorage(buildUtmLocalStorageKey(utmTagKey)) ? utils.getDataFromLocalStorage(buildUtmLocalStorageKey(utmTagKey)) : ''; + utmTagData[utmTagKey] = storage.getDataFromLocalStorage(buildUtmLocalStorageKey(utmTagKey)) ? storage.getDataFromLocalStorage(buildUtmLocalStorageKey(utmTagKey)) : ''; updateUtmTimeout(); } } diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index fe42523e737..210153548b2 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -10,6 +10,7 @@ export const GOOGLE_CONSENT = { consented_providers: ['3', '7', '11', '12', '15' export const spec = { code: BIDDER_CODE, + gvlid: 165, aliases: ['spotx'], supportedMediaTypes: [VIDEO], diff --git a/modules/staqAnalyticsAdapter.js b/modules/staqAnalyticsAdapter.js index 2bcb175e531..31b231637f4 100644 --- a/modules/staqAnalyticsAdapter.js +++ b/modules/staqAnalyticsAdapter.js @@ -5,6 +5,9 @@ import { getRefererInfo } from '../src/refererDetection.js'; import { parse } from '../src/url.js'; import * as utils from '../src/utils.js'; import { ajax } from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storageObj = getStorageManager(); const ANALYTICS_VERSION = '1.0.0'; const DEFAULT_QUEUE_TIMEOUT = 4000; @@ -212,10 +215,10 @@ const ORGANIC = '(organic)'; export let storage = { getItem: (name) => { - return utils.getDataFromLocalStorage(name); + return storageObj.getDataFromLocalStorage(name); }, setItem: (name, value) => { - utils.setDataInLocalStorage(name, value); + storageObj.setDataInLocalStorage(name, value); } }; diff --git a/modules/sublimeBidAdapter.js b/modules/sublimeBidAdapter.js index 63f851c5b4d..951b7b86ca9 100644 --- a/modules/sublimeBidAdapter.js +++ b/modules/sublimeBidAdapter.js @@ -10,6 +10,7 @@ const SUBLIME_VERSION = '0.4.0'; export const spec = { code: BIDDER_CODE, + gvlid: 114, aliases: [], /** diff --git a/modules/trionBidAdapter.js b/modules/trionBidAdapter.js index 846711a2721..37d54e60cd2 100644 --- a/modules/trionBidAdapter.js +++ b/modules/trionBidAdapter.js @@ -1,5 +1,8 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BID_REQUEST_BASE_URL = 'https://in-appadvertising.com/api/bidRequest'; const USER_SYNC_URL = 'https://in-appadvertising.com/api/userSync.html'; @@ -176,8 +179,8 @@ function handlePostMessage() { export function getStorageData(key) { var item = null; try { - if (utils.hasLocalStorage()) { - item = utils.getDataFromLocalStorage(key); + if (storage.hasLocalStorage()) { + item = storage.getDataFromLocalStorage(key); } } catch (e) { } @@ -186,8 +189,8 @@ export function getStorageData(key) { export function setStorageData(key, item) { try { - if (utils.hasLocalStorage()) { - utils.setDataInLocalStorage(key, item); + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage(key, item); } } catch (e) { } diff --git a/modules/unicornBidAdapter.js b/modules/unicornBidAdapter.js index b782c716323..cc320dc68a8 100644 --- a/modules/unicornBidAdapter.js +++ b/modules/unicornBidAdapter.js @@ -1,6 +1,9 @@ import * as utils from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'unicorn'; const UNICORN_ENDPOINT = 'https://ds.uncn.jp/pb/0/bid.json'; const UNICORN_DEFAULT_CURRENCY = 'JPY'; @@ -119,7 +122,7 @@ const interpretResponse = (serverResponse, request) => { * Get or Create Uid for First Party Cookie */ const getUid = () => { - const ck = utils.getCookie(UNICORN_PB_COOKIE_KEY); + const ck = storage.getCookie(UNICORN_PB_COOKIE_KEY); if (ck) { return JSON.parse(ck)['uid']; } else { @@ -127,7 +130,7 @@ const getUid = () => { uid: utils.generateUUID() }; const expireIn = new Date(Date.now() + 24 * 60 * 60 * 10000).toUTCString(); - utils.setCookie(UNICORN_PB_COOKIE_KEY, JSON.stringify(newCk), expireIn); + storage.setCookie(UNICORN_PB_COOKIE_KEY, JSON.stringify(newCk), expireIn); return newCk.uid; } }; diff --git a/modules/userId/index.js b/modules/userId/index.js index 89d3ee9e5c7..cad513642a5 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -115,14 +115,16 @@ import * as utils from '../../src/utils.js'; import {getGlobal} from '../../src/prebidGlobal.js'; import {gdprDataHandler} from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; -import {module} from '../../src/hook.js'; +import {module, hook} from '../../src/hook.js'; import {createEidsArray} from './eids.js'; +import { getCoreStorageManager } from '../../src/storageManager.js'; const MODULE_NAME = 'User ID'; const COOKIE = 'cookie'; const LOCAL_STORAGE = 'html5'; const DEFAULT_SYNC_DELAY = 500; const NO_AUCTION_DELAY = 0; +export const coreStorage = getCoreStorageManager('userid'); /** @type {string[]} */ let validStorageTypes = []; @@ -162,15 +164,15 @@ function setStoredValue(storage, value) { const valueStr = utils.isPlainObject(value) ? JSON.stringify(value) : value; const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString(); if (storage.type === COOKIE) { - utils.setCookie(storage.name, valueStr, expiresStr, 'Lax'); + coreStorage.setCookie(storage.name, valueStr, expiresStr, 'Lax'); if (typeof storage.refreshInSeconds === 'number') { - utils.setCookie(`${storage.name}_last`, new Date().toUTCString(), expiresStr); + coreStorage.setCookie(`${storage.name}_last`, new Date().toUTCString(), expiresStr); } } else if (storage.type === LOCAL_STORAGE) { - utils.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); - utils.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); + coreStorage.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); + coreStorage.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); if (typeof storage.refreshInSeconds === 'number') { - utils.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); + coreStorage.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); } } } catch (error) { @@ -188,15 +190,15 @@ function getStoredValue(storage, key = undefined) { let storedValue; try { if (storage.type === COOKIE) { - storedValue = utils.getCookie(storedKey); + storedValue = coreStorage.getCookie(storedKey); } else if (storage.type === LOCAL_STORAGE) { - const storedValueExp = utils.getDataFromLocalStorage(`${storage.name}_exp`); + const storedValueExp = coreStorage.getDataFromLocalStorage(`${storage.name}_exp`); // empty string means no expiration set if (storedValueExp === '') { - storedValue = utils.getDataFromLocalStorage(storedKey); + storedValue = coreStorage.getDataFromLocalStorage(storedKey); } else if (storedValueExp) { if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { - storedValue = decodeURIComponent(utils.getDataFromLocalStorage(storedKey)); + storedValue = decodeURIComponent(coreStorage.getDataFromLocalStorage(storedKey)); } } } @@ -376,6 +378,13 @@ function getUserIds() { return getCombinedSubmoduleIds(initializedSubmodules); } +/** + * This hook returns updated list of submodules which are allowed to do get user id based on TCF 2 enforcement rules configured + */ +export const validateGdprEnforcement = hook('sync', function (submodules, consentData) { + return submodules; +}, 'validateGdprEnforcement'); + /** * @param {SubmoduleContainer[]} submodules * @param {ConsentData} consentData @@ -383,12 +392,13 @@ function getUserIds() { */ function initSubmodules(submodules, consentData) { // gdpr consent with purpose one is required, otherwise exit immediately + let userIdModules = validateGdprEnforcement(submodules, consentData); if (!hasGDPRConsent(consentData)) { utils.logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); return []; } - return submodules.reduce((carry, submodule) => { + return userIdModules.reduce((carry, submodule) => { // There are two submodule configuration types to handle: storage or value // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method // 2. value: pass directly to bids @@ -534,17 +544,17 @@ export function init(config) { // list of browser enabled storage types validStorageTypes = [ - utils.localStorageIsEnabled() ? LOCAL_STORAGE : null, - utils.cookiesAreEnabled() ? COOKIE : null + coreStorage.localStorageIsEnabled() ? LOCAL_STORAGE : null, + coreStorage.cookiesAreEnabled() ? COOKIE : null ].filter(i => i !== null); // exit immediately if opt out cookie or local storage keys exists. - if (validStorageTypes.indexOf(COOKIE) !== -1 && (utils.getCookie('_pbjs_id_optout') || utils.getCookie('_pubcid_optout'))) { + if (validStorageTypes.indexOf(COOKIE) !== -1 && (coreStorage.getCookie('_pbjs_id_optout') || coreStorage.getCookie('_pubcid_optout'))) { utils.logInfo(`${MODULE_NAME} - opt-out cookie found, exit module`); return; } // _pubcid_optout is checked for compatiblility with pubCommonId - if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && (utils.getDataFromLocalStorage('_pbjs_id_optout') || utils.getDataFromLocalStorage('_pubcid_optout'))) { + if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && (coreStorage.getDataFromLocalStorage('_pbjs_id_optout') || coreStorage.getDataFromLocalStorage('_pubcid_optout'))) { utils.logInfo(`${MODULE_NAME} - opt-out localStorage found, exit module`); return; } diff --git a/modules/widespaceBidAdapter.js b/modules/widespaceBidAdapter.js index 26ced2bb802..5165b00a98c 100644 --- a/modules/widespaceBidAdapter.js +++ b/modules/widespaceBidAdapter.js @@ -1,19 +1,17 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { - cookiesAreEnabled, - hasLocalStorage, parseQueryStringParameters, parseSizesInput } from '../src/utils.js'; import includes from 'core-js/library/fn/array/includes.js'; import find from 'core-js/library/fn/array/find.js'; -import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); const BIDDER_CODE = 'widespace'; const WS_ADAPTER_VERSION = '2.0.1'; -const LOCAL_STORAGE_AVAILABLE = hasLocalStorage(); -const COOKIE_ENABLED = cookiesAreEnabled(); const LS_KEYS = { PERF_DATA: 'wsPerfData', LC_UID: 'wsLcuid', @@ -177,37 +175,37 @@ export const spec = { function storeData(data, name, stringify = true) { const value = stringify ? JSON.stringify(data) : data; - if (LOCAL_STORAGE_AVAILABLE) { - utils.setDataInLocalStorage(name, value); + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage(name, value); return true; - } else if (COOKIE_ENABLED) { + } else if (storage.cookiesAreEnabled()) { const theDate = new Date(); const expDate = new Date(theDate.setMonth(theDate.getMonth() + 12)).toGMTString(); - utils.setCookie(name, value, expDate); + storage.setCookie(name, value, expDate); return true; } } function getData(name, remove = true) { let data = []; - if (LOCAL_STORAGE_AVAILABLE) { + if (storage.hasLocalStorage()) { Object.keys(localStorage).filter((key) => { if (key.indexOf(name) > -1) { - data.push(utils.getDataFromLocalStorage(key)); + data.push(storage.getDataFromLocalStorage(key)); if (remove) { - utils.removeDataFromLocalStorage(key); + storage.removeDataFromLocalStorage(key); } } }); } - if (COOKIE_ENABLED) { + if (storage.cookiesAreEnabled()) { document.cookie.split(';').forEach((item) => { let value = item.split('='); if (value[0].indexOf(name) > -1) { data.push(value[1]); if (remove) { - utils.setCookie(value[0], '', 'Thu, 01 Jan 1970 00:00:01 GMT'); + storage.setCookie(value[0], '', 'Thu, 01 Jan 1970 00:00:01 GMT'); } } }); diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 3e8771cd8df..af3ab5ed8c1 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -9,9 +9,12 @@ import CONSTANTS from '../constants.json'; import events from '../events.js'; import includes from 'core-js/library/fn/array/includes.js'; import { ajax } from '../ajax.js'; -import { logWarn, logError, parseQueryStringParameters, delayExecution, parseSizesInput, getBidderRequest, flatten, uniques, timestamp, setDataInLocalStorage, getDataFromLocalStorage, deepAccess, isArray } from '../utils.js'; +import { logWarn, logError, parseQueryStringParameters, delayExecution, parseSizesInput, getBidderRequest, flatten, uniques, timestamp, deepAccess, isArray } from '../utils.js'; import { ADPOD } from '../mediaTypes.js'; -import { getHook } from '../hook.js'; +import { getHook, hook } from '../hook.js'; +import { getCoreStorageManager } from '../storageManager.js'; + +export const storage = getCoreStorageManager('bidderFactory'); /** * This file aims to support Adapters during the Prebid 0.x -> 1.x transition. @@ -331,22 +334,7 @@ export function newBidder(spec) { }); function registerSyncs(responses, gdprConsent, uspConsent) { - const aliasSyncEnabled = config.getConfig('userSync.aliasSyncEnabled'); - if (spec.getUserSyncs && (aliasSyncEnabled || !adapterManager.aliasRegistry[spec.code])) { - let filterConfig = config.getConfig('userSync.filterSettings'); - let syncs = spec.getUserSyncs({ - iframeEnabled: !!(filterConfig && (filterConfig.iframe || filterConfig.all)), - pixelEnabled: !!(filterConfig && (filterConfig.image || filterConfig.all)), - }, responses, gdprConsent, uspConsent); - if (syncs) { - if (!Array.isArray(syncs)) { - syncs = [syncs]; - } - syncs.forEach((sync) => { - userSync.registerSync(sync.type, spec.code, sync.url) - }); - } - } + registerSyncInner(spec, responses, gdprConsent, uspConsent); } function filterAndWarn(bid) { @@ -358,6 +346,25 @@ export function newBidder(spec) { } } +export const registerSyncInner = hook('async', function(spec, responses, gdprConsent, uspConsent) { + const aliasSyncEnabled = config.getConfig('userSync.aliasSyncEnabled'); + if (spec.getUserSyncs && (aliasSyncEnabled || !adapterManager.aliasRegistry[spec.code])) { + let filterConfig = config.getConfig('userSync.filterSettings'); + let syncs = spec.getUserSyncs({ + iframeEnabled: !!(filterConfig && (filterConfig.iframe || filterConfig.all)), + pixelEnabled: !!(filterConfig && (filterConfig.image || filterConfig.all)), + }, responses, gdprConsent, uspConsent); + if (syncs) { + if (!Array.isArray(syncs)) { + syncs = [syncs]; + } + syncs.forEach((sync) => { + userSync.registerSync(sync.type, spec.code, sync.url) + }); + } + } +}, 'registerSyncs') + export function preloadBidderMappingFile(fn, adUnits) { if (!config.getConfig('adpod.brandCategoryExclusion')) { return fn.call(this, adUnits); @@ -374,7 +381,7 @@ export function preloadBidderMappingFile(fn, adUnits) { let info = bidderSpec.getSpec().getMappingFileInfo(); let refreshInDays = (info.refreshInDays) ? info.refreshInDays : DEFAULT_REFRESHIN_DAYS; let key = (info.localStorageKey) ? info.localStorageKey : bidderSpec.getSpec().code; - let mappingData = getDataFromLocalStorage(key); + let mappingData = storage.getDataFromLocalStorage(key); if (!mappingData || timestamp() < mappingData.lastUpdated + refreshInDays * 24 * 60 * 60 * 1000) { ajax(info.url, { @@ -385,7 +392,7 @@ export function preloadBidderMappingFile(fn, adUnits) { lastUpdated: timestamp(), mapping: response.mapping } - setDataInLocalStorage(key, JSON.stringify(mapping)); + storage.setDataInLocalStorage(key, JSON.stringify(mapping)); } catch (error) { logError(`Failed to parse ${bidder} bidder translation mapping file`); } @@ -413,7 +420,7 @@ export function getIabSubCategory(bidderCode, category) { if (bidderSpec.getSpec().getMappingFileInfo) { let info = bidderSpec.getSpec().getMappingFileInfo(); let key = (info.localStorageKey) ? info.localStorageKey : bidderSpec.getBidderCode(); - let data = getDataFromLocalStorage(key); + let data = storage.getDataFromLocalStorage(key); if (data) { try { data = JSON.parse(data); diff --git a/src/config.js b/src/config.js index 8484b87b77c..24962bdff37 100644 --- a/src/config.js +++ b/src/config.js @@ -450,9 +450,14 @@ export function newConfig() { } } + function getCurrentBidder() { + return currBidder; + } + resetConfig(); return { + getCurrentBidder, getConfig, setConfig, setDefaults, diff --git a/src/prebid.js b/src/prebid.js index aa2d2493e7b..480e3afa6fc 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -13,6 +13,7 @@ import includes from 'core-js/library/fn/array/includes.js'; import { adunitCounter } from './adUnits.js'; import { isRendererRequired, executeRenderer } from './Renderer.js'; import { createBid } from './bidfactory.js'; +import { storageCallbacks } from './storageManager.js'; const $$PREBID_GLOBAL$$ = getGlobal(); const CONSTANTS = require('./constants.json'); @@ -523,6 +524,20 @@ $$PREBID_GLOBAL$$.requestBids = hook('async', function ({ bidsBackHandler, timeo return auction; }); +export function executeStorageCallbacks(fn, reqBidsConfigObj) { + runAll(storageCallbacks); + fn.call(this, reqBidsConfigObj); + function runAll(queue) { + var queued; + while ((queued = queue.shift())) { + queued(); + } + } +} + +// This hook will execute all storage callbacks which were registered before gdpr enforcement hook was added. Some bidders, user id modules use storage functions when module is parsed but gdpr enforcement hook is not added at that stage as setConfig callbacks are yet to be called. Hence for such calls we execute all the stored callbacks just before requestBids. At this hook point we will know for sure that gdprEnforcement module is added or not +$$PREBID_GLOBAL$$.requestBids.before(executeStorageCallbacks, 49); + /** * * Add adunit(s) diff --git a/src/storageManager.js b/src/storageManager.js new file mode 100644 index 00000000000..abda0ccb180 --- /dev/null +++ b/src/storageManager.js @@ -0,0 +1,309 @@ +import { hook } from './hook.js'; +import * as utils from './utils.js'; +import includes from 'core-js/library/fn/array/includes.js'; + +const moduleTypeWhiteList = ['core', 'prebid-module']; + +export let storageCallbacks = []; + +/** + * Storage options + * @typedef {Object} storageOptions + * @property {Number=} gvlid - Vendor id + * @property {string} moduleName - Module name + * @property {string=} moduleType - Module type, value can be anyone of core or prebid-module + */ + +/** + * Returns list of storage related functions with gvlid, module name and module type in its scope. + * All three argument are optional here. Below shows the usage of of these + * - GVL Id: Pass GVL id if you are a vendor + * - Module name: All modules need to pass module name + * - Module type: Some modules may need these functions but are not vendor. e.g prebid core files in src and modules like currency. + * @param {storageOptions} options + */ +export function newStorageManager({gvlid, moduleName, moduleType} = {}) { + function isValid(cb) { + if (includes(moduleTypeWhiteList, moduleType)) { + let result = { + valid: true + } + return cb(result); + } else { + let value; + let hookDetails = { + hasEnforcementHook: false + } + validateStorageEnforcement(gvlid, moduleName, hookDetails, function(result) { + if (result && result.hasEnforcementHook) { + value = cb(result); + } else { + let result = { + hasEnforcementHook: false, + valid: utils.hasDeviceAccess() + } + value = cb(result); + } + }); + return value; + } + } + + /** + * @param {string} key + * @param {string} value + * @param {string} [expires=''] + * @param {string} [sameSite='/'] + * @param {string} [domain] domain (e.g., 'example.com' or 'subdomain.example.com'). + * If not specified, defaults to the host portion of the current document location. + * If a domain is specified, subdomains are always included. + * Domain must match the domain of the JavaScript origin. Setting cookies to foreign domains will be silently ignored. + */ + const setCookie = function (key, value, expires, sameSite, domain, done) { + let cb = function (result) { + if (result && result.valid) { + const domainPortion = (domain && domain !== '') ? ` ;domain=${encodeURIComponent(domain)}` : ''; + const expiresPortion = (expires && expires !== '') ? ` ;expires=${expires}` : ''; + document.cookie = `${key}=${encodeURIComponent(value)}${expiresPortion}; path=/${domainPortion}${sameSite ? `; SameSite=${sameSite}` : ''}`; + } + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + }; + + /** + * @param {string} name + * @returns {(string|null)} + */ + const getCookie = function(name, done) { + let cb = function (result) { + if (result && result.valid) { + let m = window.document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]*)\\s*(;|$)'); + return m ? decodeURIComponent(m[2]) : null; + } + return null; + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + }; + + /** + * @returns {boolean} + */ + const localStorageIsEnabled = function (done) { + let cb = function (result) { + if (result && result.valid) { + try { + localStorage.setItem('prebid.cookieTest', '1'); + return localStorage.getItem('prebid.cookieTest') === '1'; + } catch (error) {} + } + return false; + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * @returns {boolean} + */ + const cookiesAreEnabled = function (done) { + let cb = function (result) { + if (result && result.valid) { + if (utils.checkCookieSupport()) { + return true; + } + window.document.cookie = 'prebid.cookieTest'; + return window.document.cookie.indexOf('prebid.cookieTest') !== -1; + } + return false; + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * @param {string} key + * @param {string} value + */ + const setDataInLocalStorage = function (key, value, done) { + let cb = function (result) { + if (result && result.valid) { + window.localStorage.setItem(key, value); + } + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * @param {string} key + * @returns {(string|null)} + */ + const getDataFromLocalStorage = function (key, done) { + let cb = function (result) { + if (result && result.valid) { + return window.localStorage.getItem(key); + } + return null; + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * @param {string} key + */ + const removeDataFromLocalStorage = function (key, done) { + let cb = function (result) { + if (result && result.valid) { + window.localStorage.removeItem(key); + } + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * @returns {boolean} + */ + const hasLocalStorage = function (done) { + let cb = function (result) { + if (result && result.valid) { + try { + return !!window.localStorage; + } catch (e) { + utils.logError('Local storage api disabled'); + } + } + return false; + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * Returns all cookie values from the jar whose names contain the `keyLike` + * Needs to exist in `utils.js` as it follows the StorageHandler interface defined in live-connect-js. If that module were to be removed, this function can go as well. + * @param {string} keyLike + * @return {[]} + */ + const findSimilarCookies = function(keyLike, done) { + let cb = function (result) { + if (result && result.valid) { + const all = []; + if (utils.hasDeviceAccess()) { + const cookies = document.cookie.split(';'); + while (cookies.length) { + const cookie = cookies.pop(); + let separatorIndex = cookie.indexOf('='); + separatorIndex = separatorIndex < 0 ? cookie.length : separatorIndex; + const cookieName = decodeURIComponent(cookie.slice(0, separatorIndex).replace(/^\s+/, '')); + if (cookieName.indexOf(keyLike) >= 0) { + all.push(decodeURIComponent(cookie.slice(separatorIndex + 1))); + } + } + } + return all; + } + } + + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + return { + setCookie, + getCookie, + localStorageIsEnabled, + cookiesAreEnabled, + setDataInLocalStorage, + getDataFromLocalStorage, + removeDataFromLocalStorage, + hasLocalStorage, + findSimilarCookies + } +} + +/** + * This hook validates the storage enforcement if gdprEnforcement module is included + */ +export const validateStorageEnforcement = hook('async', function(gvlid, moduleName, hookDetails, callback) { + callback(hookDetails); +}, 'validateStorageEnforcement'); + +/** + * This function returns storage functions to access cookies and localstorage. This function will bypass the gdpr enforcement requirement. Prebid as a software needs to use storage in some scenarios and is not a vendor so GDPR enforcement rules does not apply on Prebid. + * @param {string} moduleName Module name + */ +export function getCoreStorageManager(moduleName) { + return newStorageManager({moduleName: moduleName, moduleType: 'core'}); +} + +/** + * Note: Core modules or Prebid modules like Currency, SizeMapping should use getCoreStorageManager + * This function returns storage functions to access cookies and localstorage. Bidders and User id modules should import this and use it in their module if needed. GVL ID and Module name are optional param but gvl id is needed for when gdpr enforcement module is used. + * @param {Number=} gvlid Vendor id + * @param {string=} moduleName BidderCode or module name + */ +export function getStorageManager(gvlid, moduleName) { + return newStorageManager({gvlid: gvlid, moduleName: moduleName}); +} + +export function resetData() { + storageCallbacks = []; +} diff --git a/src/userSync.js b/src/userSync.js index e19f64d8082..5da22254f2c 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -1,6 +1,7 @@ import * as utils from './utils.js'; import { config } from './config.js'; import includes from 'core-js/library/fn/array/includes.js'; +import { getCoreStorageManager } from './storageManager.js'; export const USERSYNC_DEFAULT_CONFIG = { syncEnabled: true, @@ -20,6 +21,8 @@ config.setDefaults({ 'userSync': utils.deepClone(USERSYNC_DEFAULT_CONFIG) }); +const storage = getCoreStorageManager('usersync'); + /** * Factory function which creates a new UserSyncPool. * @@ -299,7 +302,7 @@ export function newUserSync(userSyncDependencies) { return publicApi; } -const browserSupportsCookies = !utils.isSafariBrowser() && utils.cookiesAreEnabled(); +const browserSupportsCookies = !utils.isSafariBrowser() && storage.cookiesAreEnabled(); export const userSync = newUserSync({ config: config.getConfig('userSync'), diff --git a/src/utils.js b/src/utils.js index 9b5b5b7bdac..d9c4e5ea0f8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -844,86 +844,6 @@ export function checkCookieSupport() { } } -/** - * @returns {boolean} - */ -export function cookiesAreEnabled() { - if (hasDeviceAccess()) { - if (internal.checkCookieSupport()) { - return true; - } - window.document.cookie = 'prebid.cookieTest'; - return window.document.cookie.indexOf('prebid.cookieTest') !== -1; - } - return false; -} - -/** - * @param {string} name - * @returns {(string|null)} - */ -export function getCookie(name) { - if (hasDeviceAccess()) { - let m = window.document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]*)\\s*(;|$)'); - return m ? decodeURIComponent(m[2]) : null; - } - return null; -} - -/** - * @param {string} key - * @param {string} value - * @param {string} [expires=''] - * @param {string} [sameSite='/'] - * @param {string} [domain] domain (e.g., 'example.com' or 'subdomain.example.com'). - * If not specified, defaults to the host portion of the current document location. - * If a domain is specified, subdomains are always included. - * Domain must match the domain of the JavaScript origin. Setting cookies to foreign domains will be silently ignored. - */ -export function setCookie(key, value, expires, sameSite, domain) { - if (hasDeviceAccess()) { - const domainPortion = (domain && domain !== '') ? ` ;domain=${encodeURIComponent(domain)}` : ''; - const expiresPortion = (expires && expires !== '') ? ` ;expires=${expires}` : ''; - document.cookie = `${key}=${encodeURIComponent(value)}${expiresPortion}; path=/${domainPortion}${sameSite ? `; SameSite=${sameSite}` : ''}`; - } -} - -/** - * Returns all cookie values from the jar whose names contain the `keyLike` - * Needs to exist in `utils.js` as it follows the StorageHandler interface defined in live-connect-js. If that module were to be removed, this function can go as well. - * @param {string} keyLike - * @return {[]} - */ -export function findSimilarCookies(keyLike) { - const all = []; - if (hasDeviceAccess()) { - const cookies = document.cookie.split(';'); - while (cookies.length) { - const cookie = cookies.pop(); - let separatorIndex = cookie.indexOf('='); - separatorIndex = separatorIndex < 0 ? cookie.length : separatorIndex; - const cookieName = decodeURIComponent(cookie.slice(0, separatorIndex).replace(/^\s+/, '')); - if (cookieName.indexOf(keyLike) >= 0) { - all.push(decodeURIComponent(cookie.slice(separatorIndex + 1))); - } - } - } - return all; -} - -/** - * @returns {boolean} - */ -export function localStorageIsEnabled () { - if (hasDeviceAccess()) { - try { - localStorage.setItem('prebid.cookieTest', '1'); - return localStorage.getItem('prebid.cookieTest') === '1'; - } catch (error) {} - } - return false; -} - /** * Given a function, return a function which only executes the original after * it's been called numRequiredCalls times. @@ -1260,50 +1180,6 @@ export function convertTypes(types, params) { return params; } -/** - * @param {string} key - * @param {string} value - */ -export function setDataInLocalStorage(key, value) { - if (hasLocalStorage()) { - window.localStorage.setItem(key, value); - } -} - -/** - * @param {string} key - * @returns {(string|null)} - */ -export function getDataFromLocalStorage(key) { - if (hasLocalStorage()) { - return window.localStorage.getItem(key); - } - return null; -} - -/** - * @param {string} key - */ -export function removeDataFromLocalStorage(key) { - if (hasLocalStorage()) { - window.localStorage.removeItem(key); - } -} - -/** - * @returns {boolean} - */ -export function hasLocalStorage() { - if (hasDeviceAccess()) { - try { - return !!window.localStorage; - } catch (e) { - logError('Local storage api disabled'); - } - } - return false; -} - export function isArrayOfNums(val, size) { return (isArray(val)) && ((size) ? val.length === size : true) && (val.every(v => isInteger(v))); } diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index c61b1cf1979..6febb8ecc9a 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -624,6 +624,7 @@ describe('AppNexusAdapter', function () { bidderRequest.bids = bidRequests; const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.options).to.be.empty; const payload = JSON.parse(request.data); expect(payload.gdpr_consent).to.exist; @@ -774,6 +775,32 @@ describe('AppNexusAdapter', function () { config.getConfig.restore(); }); + + it('should set withCredentials to false if purpose 1 consent is not given', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'appnexus', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true, + apiVersion: 2, + vendorData: { + vendor: { + consents: { + 32: false + } + } + } + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.options).to.deep.equal({withCredentials: false}); + }); }) describe('interpretResponse', function () { diff --git a/test/spec/modules/bidfluenceBidAdapter_spec.js b/test/spec/modules/bidfluenceBidAdapter_spec.js index ff33715a176..6b3a0c2b044 100644 --- a/test/spec/modules/bidfluenceBidAdapter_spec.js +++ b/test/spec/modules/bidfluenceBidAdapter_spec.js @@ -51,30 +51,30 @@ describe('Bidfluence Adapter test', () => { }); describe('buildRequests', function () { - const request = spec.buildRequests(validBidRequests, bidderRequest); - it('sends bid request to our endpoint via POST', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); expect(request.method).to.equal('POST'); - }); + const payload = JSON.parse(request.data); - const payload = JSON.parse(request.data); - - expect(payload.bids[0].bid).to.equal(validBidRequests[0].bidId); - expect(payload.azr).to.equal(true); - expect(payload.ck).to.not.be.undefined; - expect(payload.bids[0].tid).to.equal(PLACEMENT_ID); - expect(payload.bids[0].pid).to.equal(PUB_ID); - expect(payload.bids[0].rp).to.be.a('number'); - expect(payload.re).to.not.be.undefined; - expect(payload.st).to.not.be.undefined; - expect(payload.tz).to.not.be.undefined; - expect(payload.sr).to.not.be.undefined; - expect(payload.vp).to.not.be.undefined; - expect(payload.sdt).to.not.be.undefined; - expect(payload.bids[0].w).to.equal('300'); - expect(payload.bids[0].h).to.equal('250'); + expect(payload.bids[0].bid).to.equal(validBidRequests[0].bidId); + expect(payload.azr).to.equal(true); + expect(payload.ck).to.not.be.undefined; + expect(payload.bids[0].tid).to.equal(PLACEMENT_ID); + expect(payload.bids[0].pid).to.equal(PUB_ID); + expect(payload.bids[0].rp).to.be.a('number'); + expect(payload.re).to.not.be.undefined; + expect(payload.st).to.not.be.undefined; + expect(payload.tz).to.not.be.undefined; + expect(payload.sr).to.not.be.undefined; + expect(payload.vp).to.not.be.undefined; + expect(payload.sdt).to.not.be.undefined; + expect(payload.bids[0].w).to.equal('300'); + expect(payload.bids[0].h).to.equal('250'); + }); it('sends gdpr info if exists', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); expect(payload.gdpr).to.equal(true); expect(payload.gdprc).to.equal(CONSENT_STRING); }); diff --git a/test/spec/modules/categoryTranslation_spec.js b/test/spec/modules/categoryTranslation_spec.js index f8684c0d7c0..555fe3d6357 100644 --- a/test/spec/modules/categoryTranslation_spec.js +++ b/test/spec/modules/categoryTranslation_spec.js @@ -1,4 +1,4 @@ -import { getAdserverCategoryHook, initTranslation } from 'modules/categoryTranslation.js'; +import { getAdserverCategoryHook, initTranslation, storage } from 'modules/categoryTranslation.js'; import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; import { expect } from 'chai'; @@ -9,7 +9,7 @@ describe('category translation', function () { beforeEach(function () { fakeTranslationServer = sinon.fakeServer.create(); - getLocalStorageStub = sinon.stub(utils, 'getDataFromLocalStorage'); + getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); afterEach(function() { diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index 7ad37a91afe..3d6a6be9fa1 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec} from 'modules/conversantBidAdapter.js'; +import {spec, storage} from 'modules/conversantBidAdapter.js'; import * as utils from 'src/utils.js'; import { createEidsArray } from 'modules/userId/eids.js'; @@ -508,7 +508,7 @@ describe('Conversant adapter tests', function() { const requests = utils.deepClone(bidRequests); // add a pubcid cookie - utils.setCookie(ID_NAME, '12345', expStr(TIMEOUT)); + storage.setCookie(ID_NAME, '12345', expStr(TIMEOUT)); // construct http post payload const payload = spec.buildRequests(requests).data; @@ -521,7 +521,7 @@ describe('Conversant adapter tests', function() { requests[0].params.pubcid_name = CUSTOM_ID_NAME; // add a pubcid cookie - utils.setCookie(CUSTOM_ID_NAME, '12345', expStr(TIMEOUT)); + storage.setCookie(CUSTOM_ID_NAME, '12345', expStr(TIMEOUT)); // construct http post payload const payload = spec.buildRequests(requests).data; @@ -533,8 +533,8 @@ describe('Conversant adapter tests', function() { const requests = utils.deepClone(bidRequests); // add a pubcid in local storage - utils.setDataInLocalStorage(ID_NAME + EXP, ''); - utils.setDataInLocalStorage(ID_NAME, 'abcde'); + storage.setDataInLocalStorage(ID_NAME + EXP, ''); + storage.setDataInLocalStorage(ID_NAME, 'abcde'); // construct http post payload const payload = spec.buildRequests(requests).data; @@ -546,8 +546,8 @@ describe('Conversant adapter tests', function() { const requests = utils.deepClone(bidRequests); // add a pubcid in local storage - utils.setDataInLocalStorage(ID_NAME + EXP, expStr(TIMEOUT)); - utils.setDataInLocalStorage(ID_NAME, 'fghijk'); + storage.setDataInLocalStorage(ID_NAME + EXP, expStr(TIMEOUT)); + storage.setDataInLocalStorage(ID_NAME, 'fghijk'); // construct http post payload const payload = spec.buildRequests(requests).data; @@ -559,8 +559,8 @@ describe('Conversant adapter tests', function() { const requests = utils.deepClone(bidRequests); // add a pubcid in local storage - utils.setDataInLocalStorage(ID_NAME + EXP, expStr(-TIMEOUT)); - utils.setDataInLocalStorage(ID_NAME, 'lmnopq'); + storage.setDataInLocalStorage(ID_NAME + EXP, expStr(-TIMEOUT)); + storage.setDataInLocalStorage(ID_NAME, 'lmnopq'); // construct http post payload const payload = spec.buildRequests(requests).data; @@ -573,8 +573,8 @@ describe('Conversant adapter tests', function() { requests[0].params.pubcid_name = CUSTOM_ID_NAME; // add a pubcid in local storage - utils.setDataInLocalStorage(CUSTOM_ID_NAME + EXP, expStr(TIMEOUT)); - utils.setDataInLocalStorage(CUSTOM_ID_NAME, 'fghijk'); + storage.setDataInLocalStorage(CUSTOM_ID_NAME + EXP, expStr(TIMEOUT)); + storage.setDataInLocalStorage(CUSTOM_ID_NAME, 'fghijk'); // construct http post payload const payload = spec.buildRequests(requests).data; diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index c36852251f8..dee9a0563e2 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -1,4 +1,4 @@ -import { criteoIdSubmodule } from 'modules/criteoIdSystem.js'; +import { criteoIdSubmodule, storage } from 'modules/criteoIdSystem.js'; import * as utils from 'src/utils.js'; import * as ajaxLib from 'src/ajax.js'; import * as urlLib from 'src/url.js'; @@ -27,11 +27,11 @@ describe('CriteoId module', function () { let triggerPixelStub; beforeEach(function (done) { - getCookieStub = sinon.stub(utils, 'getCookie'); - setCookieStub = sinon.stub(utils, 'setCookie'); - getLocalStorageStub = sinon.stub(utils, 'getDataFromLocalStorage'); - setLocalStorageStub = sinon.stub(utils, 'setDataInLocalStorage'); - removeFromLocalStorageStub = sinon.stub(utils, 'removeDataFromLocalStorage'); + getCookieStub = sinon.stub(storage, 'getCookie'); + setCookieStub = sinon.stub(storage, 'setCookie'); + getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + setLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); + removeFromLocalStorageStub = sinon.stub(storage, 'removeDataFromLocalStorage'); timeStampStub = sinon.stub(utils, 'timestamp').returns(nowTimestamp); ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(mockResponse('{}')); parseUrlStub = sinon.stub(urlLib, 'parse').returns({protocol: 'https', hostname: 'testdev.com'}) diff --git a/test/spec/modules/emoteevBidAdapter_spec.js b/test/spec/modules/emoteevBidAdapter_spec.js index b967eee33b6..bf5bcc9e063 100644 --- a/test/spec/modules/emoteevBidAdapter_spec.js +++ b/test/spec/modules/emoteevBidAdapter_spec.js @@ -47,6 +47,7 @@ import { validateExternalId, VENDOR_ID, WALLPAPER, + storage } from 'modules/emoteevBidAdapter.js'; import * as url from '../../../src/url.js'; import * as utils from '../../../src/utils.js'; @@ -749,7 +750,7 @@ describe('emoteevBidAdapter', function () { }); beforeEach(function () { triggerPixelStub = sinon.stub(utils, 'triggerPixel'); - getCookieSpy = sinon.spy(utils, 'getCookie'); + getCookieSpy = sinon.spy(storage, 'getCookie'); getConfigSpy = sinon.spy(config, 'getConfig'); getParameterByNameSpy = sinon.spy(utils, 'getParameterByName'); }); @@ -776,7 +777,7 @@ describe('emoteevBidAdapter', function () { }; spec.isBidRequestValid(validBidRequest); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // sinon.assert.notCalled(config.getConfig); sinon.assert.notCalled(utils.getParameterByName); }); @@ -786,7 +787,7 @@ describe('emoteevBidAdapter', function () { const invalidBidRequest = {}; spec.isBidRequestValid(invalidBidRequest); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // disabling these getConfig tests as they have been flaky in browserstack testing // sinon.assert.notCalled(config.getConfig); sinon.assert.notCalled(utils.getParameterByName); @@ -796,7 +797,7 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.buildRequests(cannedValidBidRequests, cannedBidderRequest); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // sinon.assert.callCount(config.getConfig, 3); sinon.assert.callCount(utils.getParameterByName, 2); }); @@ -805,7 +806,7 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.interpretResponse(serverResponse); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // sinon.assert.notCalled(config.getConfig); sinon.assert.notCalled(utils.getParameterByName); }); @@ -815,7 +816,7 @@ describe('emoteevBidAdapter', function () { const bidObject = serverResponse.body[0]; spec.onBidWon(bidObject); sinon.assert.calledOnce(utils.triggerPixel); - sinon.assert.calledOnce(utils.getCookie); + sinon.assert.calledOnce(storage.getCookie); // sinon.assert.calledOnce(config.getConfig); sinon.assert.calledOnce(utils.getParameterByName); }); @@ -824,7 +825,7 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.onTimeout(cannedValidBidRequests[0]); sinon.assert.calledOnce(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // sinon.assert.calledOnce(config.getConfig); sinon.assert.calledOnce(utils.getParameterByName); }); @@ -833,7 +834,7 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.getUserSyncs({}); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // sinon.assert.calledOnce(config.getConfig); sinon.assert.calledOnce(utils.getParameterByName); }); diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index e9d478f911f..ff03bf033af 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; -import { spec } from 'modules/eplanningBidAdapter.js'; +import { spec, storage } from 'modules/eplanningBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import * as utils from 'src/utils.js'; describe('E-Planning Adapter', function () { const adapter = newBidder('spec'); @@ -559,10 +558,10 @@ describe('E-Planning Adapter', function () { }); } beforeEach(function () { - getLocalStorageSpy = sandbox.spy(utils, 'getDataFromLocalStorage'); - setDataInLocalStorageSpy = sandbox.spy(utils, 'setDataInLocalStorage'); + getLocalStorageSpy = sandbox.spy(storage, 'getDataFromLocalStorage'); + setDataInLocalStorageSpy = sandbox.spy(storage, 'setDataInLocalStorage'); - hasLocalStorageStub = sandbox.stub(utils, 'hasLocalStorage'); + hasLocalStorageStub = sandbox.stub(storage, 'hasLocalStorage'); hasLocalStorageStub.returns(true); clock = sandbox.useFakeTimers(); @@ -603,7 +602,7 @@ describe('E-Planning Adapter', function () { sinon.assert.calledWith(getLocalStorageSpy, storageIdRender); sinon.assert.calledWith(setDataInLocalStorageSpy, storageIdRender); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); }); context('when element is fully in view', function() { @@ -617,29 +616,29 @@ describe('E-Planning Adapter', function () { expect(respuesta.data.vs).to.equal('F'); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal('1'); }); it('when you have more than four render', function() { - utils.setDataInLocalStorage(storageIdRender, 4); + storage.setDataInLocalStorage(storageIdRender, 4); respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); expect(respuesta.data.vs).to.equal('0'); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('5'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('5'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal('1'); }); it('when you have more than four render and already record visibility', function() { - utils.setDataInLocalStorage(storageIdRender, 4); - utils.setDataInLocalStorage(storageIdView, 4); + storage.setDataInLocalStorage(storageIdRender, 4); + storage.setDataInLocalStorage(storageIdView, 4); respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); expect(respuesta.data.vs).to.equal('a'); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('5'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal('5'); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('5'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal('5'); }); }); @@ -654,17 +653,17 @@ describe('E-Planning Adapter', function () { clock.tick(1005); expect(respuesta.data.vs).to.equal('F'); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); it('when you have more than four render', function() { - utils.setDataInLocalStorage(storageIdRender, 4); + storage.setDataInLocalStorage(storageIdRender, 4); respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); expect(respuesta.data.vs).to.equal('0'); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('5'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('5'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); @@ -675,16 +674,16 @@ describe('E-Planning Adapter', function () { respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal('1'); }); it('you should not register visibility with less than 50%', function() { createPartiallyInvisibleElement(); respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); context('when width or height of the element is zero', function() { @@ -696,16 +695,16 @@ describe('E-Planning Adapter', function () { spec.buildRequests(bidRequests, bidderRequest) clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); it('if the height is zero but the width is within the range', function() { element.style.height = '0px'; spec.buildRequests(bidRequests, bidderRequest) clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); it('if both are zero', function() { element.style.height = '0px'; @@ -713,8 +712,8 @@ describe('E-Planning Adapter', function () { spec.buildRequests(bidRequests, bidderRequest) clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); context('when tab is inactive', function() { @@ -723,8 +722,8 @@ describe('E-Planning Adapter', function () { focusStub.returns(false); spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); context('segmentBeginsBeforeTheVisibleRange', function() { @@ -732,16 +731,16 @@ describe('E-Planning Adapter', function () { createElementOutOfRange(); spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); context('when there are multiple adunit', function() { let respuesta; beforeEach(function () { [ADUNIT_CODE_VIEW, ADUNIT_CODE_VIEW2, ADUNIT_CODE_VIEW3].forEach(ac => { - utils.setDataInLocalStorage('pbsr_' + ac, 5); - utils.setDataInLocalStorage('pbvi_' + ac, 5); + storage.setDataInLocalStorage('pbsr_' + ac, 5); + storage.setDataInLocalStorage('pbvi_' + ac, 5); }); }); afterEach(function () { @@ -761,8 +760,8 @@ describe('E-Planning Adapter', function () { respuesta = spec.buildRequests(bidRequestMultiple, bidderRequest); clock.tick(1005); [ADUNIT_CODE_VIEW, ADUNIT_CODE_VIEW2, ADUNIT_CODE_VIEW3].forEach(ac => { - expect(utils.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); - expect(utils.getDataFromLocalStorage('pbvi_' + ac)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbvi_' + ac)).to.equal('6'); }); expect('aaa').to.equal(respuesta.data.vs); }); @@ -774,8 +773,8 @@ describe('E-Planning Adapter', function () { respuesta = spec.buildRequests(bidRequestMultiple, bidderRequest); clock.tick(1005); [ADUNIT_CODE_VIEW, ADUNIT_CODE_VIEW2, ADUNIT_CODE_VIEW3].forEach(ac => { - expect(utils.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); - expect(utils.getDataFromLocalStorage('pbvi_' + ac)).to.equal('5'); + expect(storage.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbvi_' + ac)).to.equal('5'); }); expect('aaa').to.equal(respuesta.data.vs); @@ -787,11 +786,11 @@ describe('E-Planning Adapter', function () { respuesta = spec.buildRequests(bidRequestMultiple, bidderRequest); clock.tick(1005); - expect(utils.getDataFromLocalStorage('pbsr_' + ADUNIT_CODE_VIEW)).to.equal('6'); - expect(utils.getDataFromLocalStorage('pbvi_' + ADUNIT_CODE_VIEW)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbsr_' + ADUNIT_CODE_VIEW)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbvi_' + ADUNIT_CODE_VIEW)).to.equal('6'); [ADUNIT_CODE_VIEW2, ADUNIT_CODE_VIEW3].forEach(ac => { - expect(utils.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); - expect(utils.getDataFromLocalStorage('pbvi_' + ac)).to.equal('5'); + expect(storage.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbvi_' + ac)).to.equal('5'); }); expect('aaa').to.equal(respuesta.data.vs); }); diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js new file mode 100644 index 00000000000..5b46441cbbb --- /dev/null +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -0,0 +1,418 @@ +import { deviceAccessHook, setEnforcementConfig, userSyncHook, userIdHook } from 'modules/gdprEnforcement.js'; +import { config } from 'src/config.js'; +import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; +import * as utils from 'src/utils.js'; +import { validateStorageEnforcement } from 'src/storageManager.js'; +import { executeStorageCallbacks } from 'src/prebid.js'; + +describe('gdpr enforcement', function() { + let nextFnSpy; + let logWarnSpy; + let gdprDataHandlerStub; + let staticConfig = { + cmpApi: 'static', + timeout: 7500, + allowAuctionWithoutConsent: false, + consentData: { + getTCData: { + 'tcString': 'COuqj-POu90rDBcBkBENAZCgAPzAAAPAACiQFwwBAABAA1ADEAbQC4YAYAAgAxAG0A', + 'cmpId': 92, + 'cmpVersion': 100, + 'tcfPolicyVersion': 2, + 'gdprApplies': true, + 'isServiceSpecific': true, + 'useNonStandardStacks': false, + 'purposeOneTreatment': false, + 'publisherCC': 'US', + 'cmpStatus': 'loaded', + 'eventStatus': 'tcloaded', + 'outOfBand': { + 'allowedVendors': {}, + 'discloseVendors': {} + }, + 'purpose': { + 'consents': { + '1': true, + '2': true, + '3': true + }, + 'legitimateInterests': { + '1': false, + '2': false, + '3': false + } + }, + 'vendor': { + 'consents': { + '1': true, + '2': true, + '3': false + }, + 'legitimateInterests': { + '1': false, + '2': true, + '3': false, + '4': false, + '5': false + } + }, + 'specialFeatureOptins': { + '1': false, + '2': false + }, + 'restrictions': {}, + 'publisher': { + 'consents': { + '1': false, + '2': false, + '3': false + }, + 'legitimateInterests': { + '1': false, + '2': false, + '3': false + }, + 'customPurpose': { + 'consents': {}, + 'legitimateInterests': {} + } + } + } + } + }; + + after(function() { + validateStorageEnforcement.getHooks({hook: deviceAccessHook}).remove(); + $$PREBID_GLOBAL$$.requestBids.getHooks({hook: executeStorageCallbacks}).remove(); + }) + + describe('deviceAccessHook', function() { + beforeEach(function() { + nextFnSpy = sinon.spy(); + gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + logWarnSpy = sinon.spy(utils, 'logWarn'); + }); + afterEach(function() { + config.resetConfig(); + gdprDataHandler.getConsentData.restore(); + logWarnSpy.restore(); + }); + it('should not allow device access when device access flag is set to false', function() { + config.setConfig({ + deviceAccess: false, + consentManagement: { + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: false, + vendorExceptions: ['appnexus', 'rubicon'] + }] + } + } + }); + + deviceAccessHook(nextFnSpy); + expect(nextFnSpy.calledOnce).to.equal(true); + let result = { + hasEnforcementHook: true, + valid: false + } + expect(nextFnSpy.calledWith(undefined, result)); + }); + + it('should only check for consent for vendor exceptions when enforcePurpose and enforceVendor are false', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: false, + vendorExceptions: ['appnexus'] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.gdprApplies = true; + consentData.apiVersion = 2; + gdprDataHandlerStub.returns(consentData); + + deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, 5, 'rubicon'); + expect(logWarnSpy.callCount).to.equal(0); + }); + + it('should check consent for all vendors when enforcePurpose and enforceVendor are true', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.gdprApplies = true; + consentData.apiVersion = 2; + gdprDataHandlerStub.returns(consentData); + + deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, 3, 'rubicon'); + expect(logWarnSpy.callCount).to.equal(1); + }); + + it('should allow device access when gdprApplies is false and hasDeviceAccess flag is true', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.gdprApplies = false; + consentData.apiVersion = 2; + gdprDataHandlerStub.returns(consentData); + + deviceAccessHook(nextFnSpy, 1, 'appnexus'); + expect(nextFnSpy.calledOnce).to.equal(true); + let result = { + hasEnforcementHook: true, + valid: true + } + expect(nextFnSpy.calledWith(undefined, result)); + }); + }); + + describe('userSyncHook', function() { + let curBidderStub; + let adapterManagerStub; + + beforeEach(function() { + gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + logWarnSpy = sinon.spy(utils, 'logWarn'); + curBidderStub = sinon.stub(config, 'getCurrentBidder'); + adapterManagerStub = sinon.stub(adapterManager, 'getBidAdapter'); + nextFnSpy = sinon.spy(); + }); + + afterEach(function() { + config.getCurrentBidder.restore(); + config.resetConfig(); + gdprDataHandler.getConsentData.restore(); + adapterManager.getBidAdapter.restore(); + logWarnSpy.restore(); + }); + + it('should allow bidder to do user sync if consent is true', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: true, + vendorExceptions: ['sampleBidder2'] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.gdprApplies = true; + consentData.apiVersion = 2; + gdprDataHandlerStub.returns(consentData); + + curBidderStub.returns('sampleBidder1'); + adapterManagerStub.withArgs('sampleBidder1').returns({ + getSpec: function() { + return { + 'gvlid': 1 + } + } + }); + userSyncHook(nextFnSpy); + + curBidderStub.returns('sampleBidder2'); + adapterManagerStub.withArgs('sampleBidder2').returns({ + getSpec: function() { + return { + 'gvlid': 3 + } + } + }); + userSyncHook(nextFnSpy); + expect(nextFnSpy.calledTwice).to.equal(true); + }); + + it('should not allow bidder to do user sync if user has denied consent', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: true, + vendorExceptions: [] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.apiVersion = 2; + consentData.gdprApplies = true; + gdprDataHandlerStub.returns(consentData); + + curBidderStub.returns('sampleBidder1'); + adapterManagerStub.withArgs('sampleBidder1').returns({ + getSpec: function() { + return { + 'gvlid': 1 + } + } + }); + userSyncHook(nextFnSpy); + + curBidderStub.returns('sampleBidder2'); + adapterManagerStub.withArgs('sampleBidder2').returns({ + getSpec: function() { + return { + 'gvlid': 3 + } + } + }); + userSyncHook(nextFnSpy); + expect(nextFnSpy.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(1); + }); + + it('should not check vendor consent when enforceVendor is false', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: true, + enforceVendor: false, + vendorExceptions: ['sampleBidder1'] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.apiVersion = 2; + consentData.gdprApplies = true; + gdprDataHandlerStub.returns(consentData); + + curBidderStub.returns('sampleBidder1'); + adapterManagerStub.withArgs('sampleBidder1').returns({ + getSpec: function() { + return { + 'gvlid': 1 + } + } + }); + userSyncHook(nextFnSpy); + + curBidderStub.returns('sampleBidder2'); + adapterManagerStub.withArgs('sampleBidder2').returns({ + getSpec: function() { + return { + 'gvlid': 3 + } + } + }); + userSyncHook(nextFnSpy); + expect(nextFnSpy.calledTwice).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + }); + }); + + describe('userIdHook', function() { + beforeEach(function() { + logWarnSpy = sinon.spy(utils, 'logWarn'); + nextFnSpy = sinon.spy(); + }); + afterEach(function() { + config.resetConfig(); + logWarnSpy.restore(); + }); + it('should allow user id module if consent is given', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: true, + vendorExceptions: [] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.apiVersion = 2; + consentData.gdprApplies = true; + let submodules = [{ + submodule: { + gvlid: 1, + name: 'sampleUserId' + } + }] + userIdHook(nextFnSpy, submodules, consentData); + expect(nextFnSpy.calledOnce).to.equal(true); + }); + + it('should allow userId module if gdpr not in scope', function() { + let submodules = [{ + submodule: { + gvlid: 1, + name: 'sampleUserId' + } + }]; + let consentData = null; + userIdHook(nextFnSpy, submodules, consentData); + expect(nextFnSpy.calledOnce).to.equal(true); + expect(nextFnSpy.calledWith(undefined, submodules, consentData)); + }); + + it('should not allow user id module if user denied consent', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: true, + vendorExceptions: [] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.apiVersion = 2; + consentData.gdprApplies = true; + let submodules = [{ + submodule: { + gvlid: 1, + name: 'sampleUserId' + } + }, { + submodule: { + gvlid: 3, + name: 'sampleUserId1' + } + }] + userIdHook(nextFnSpy, submodules, consentData); + expect(logWarnSpy.callCount).to.equal(1); + let expectedSubmodules = [{ + submodule: { + gvlid: 1, + name: 'sampleUserId' + } + }] + expect(nextFnSpy.calledWith(undefined, expectedSubmodules, consentData)); + }); + }); +}); diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index 3df1853c9f9..c3c9c8dc38d 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,4 +1,4 @@ -import {liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule} from 'modules/liveIntentIdSystem.js'; +import {liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage} from 'modules/liveIntentIdSystem.js'; import * as utils from 'src/utils.js'; import {uspDataHandler} from '../../../src/adapterManager.js'; import {server} from 'test/mocks/xhr.js'; @@ -17,8 +17,8 @@ describe('LiveIntentId', function () { beforeEach(function () { imgStub = sinon.stub(window, 'Image').returns(pixel); - getCookieStub = sinon.stub(utils, 'getCookie'); - getDataFromLocalStorageStub = sinon.stub(utils, 'getDataFromLocalStorage'); + getCookieStub = sinon.stub(storage, 'getCookie'); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); logErrorStub = sinon.stub(utils, 'logError'); consentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); }); diff --git a/test/spec/modules/livewrappedBidAdapter_spec.js b/test/spec/modules/livewrappedBidAdapter_spec.js index a48c00b6890..7b26f5b533d 100644 --- a/test/spec/modules/livewrappedBidAdapter_spec.js +++ b/test/spec/modules/livewrappedBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec} from 'modules/livewrappedBidAdapter.js'; +import {spec, storage} from 'modules/livewrappedBidAdapter.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import { BANNER, NATIVE } from 'src/mediaTypes.js'; @@ -88,7 +88,7 @@ describe('Livewrapped adapter tests', function () { describe('buildRequests', function() { it('should make a well-formed single request object', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let result = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = JSON.parse(result.data); @@ -116,7 +116,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed multiple request object', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let multiplebidRequest = clone(bidderRequest); multiplebidRequest.bids.push(clone(bidderRequest.bids[0])); multiplebidRequest.bids[1].adUnitCode = 'box_d_1'; @@ -156,7 +156,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with AdUnitName', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); testbidRequest.bids[0].params.adUnitName = 'caller id 1'; delete testbidRequest.bids[0].params.adUnitId; @@ -186,7 +186,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with less parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -215,7 +215,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with less parameters, no publisherId', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -244,7 +244,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with app parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -275,7 +275,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with debug parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -306,7 +306,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with optional parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -336,7 +336,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with ad blocker revovered parameter', function() { sandbox.stub(utils, 'getWindowTop').returns({ I12C: { Morph: 1 } }); sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -364,7 +364,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with native only parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -393,7 +393,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with native and banner parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -423,7 +423,7 @@ describe('Livewrapped adapter tests', function () { it('should use mediaTypes.banner.sizes before legacy sizes', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -451,7 +451,7 @@ describe('Livewrapped adapter tests', function () { it('should pass gdpr true parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testRequest = clone(bidderRequest); testRequest.gdprConsent = { gdprApplies: true, @@ -486,7 +486,7 @@ describe('Livewrapped adapter tests', function () { it('should pass gdpr false parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testRequest = clone(bidderRequest); testRequest.gdprConsent = { gdprApplies: false @@ -518,7 +518,7 @@ describe('Livewrapped adapter tests', function () { }); it('should pass no cookie support', function() { - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => false); sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); let result = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = JSON.parse(result.data); @@ -546,7 +546,7 @@ describe('Livewrapped adapter tests', function () { }); it('should pass no cookie support Safari', function() { - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); sandbox.stub(utils, 'isSafariBrowser').callsFake(() => true); let result = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = JSON.parse(result.data); @@ -605,7 +605,7 @@ describe('Livewrapped adapter tests', function () { it('should make use of pubcid if available', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; testbidRequest.bids[0].crumbs = {pubcid: 'pubcid 123'}; @@ -636,7 +636,7 @@ describe('Livewrapped adapter tests', function () { it('should make userId take precedence over pubcid', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); testbidRequest.bids[0].crumbs = {pubcid: 'pubcid 123'}; let result = spec.buildRequests(testbidRequest.bids, testbidRequest); @@ -667,7 +667,7 @@ describe('Livewrapped adapter tests', function () { it('should make use of Id5-Id if available', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; testbidRequest.bids[0].userId = {}; @@ -686,7 +686,7 @@ describe('Livewrapped adapter tests', function () { it('should make use of publisher common Id if available', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; testbidRequest.bids[0].userId = {}; diff --git a/test/spec/modules/newborntownWebBidAdapter_spec.js b/test/spec/modules/newborntownWebBidAdapter_spec.js index 84937d00012..3d3285328fe 100644 --- a/test/spec/modules/newborntownWebBidAdapter_spec.js +++ b/test/spec/modules/newborntownWebBidAdapter_spec.js @@ -54,8 +54,8 @@ describe('NewborntownWebAdapter', function() { 'timeout': 9000, 'start': 1573123289383 } - const request = spec.buildRequests(bidderRequest['bids'], bidderRequest); it('Returns POST method', function () { + const request = spec.buildRequests(bidderRequest['bids'], bidderRequest); expect(request[0].method).to.equal('POST'); expect(request[0].url.indexOf('//us-west.solortb.com/adx/api/rtb?from=4') !== -1).to.equal(true); expect(request[0].data).to.exist; diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 936b4c3a824..93415126a0a 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -3,6 +3,9 @@ import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; import { parrableIdSubmodule } from 'modules/parrableIdSystem.js'; +import { newStorageManager } from 'src/storageManager.js'; + +const storage = newStorageManager(); const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; const P_COOKIE_NAME = '_parrable_eid'; @@ -52,7 +55,7 @@ describe('Parrable ID System', function() { it('should append parrableid to bid request', function(done) { // simulate existing browser local storage values - utils.setCookie( + storage.setCookie( P_COOKIE_NAME, P_COOKIE_VALUE, (new Date(Date.now() + 5000).toUTCString()) @@ -69,7 +72,7 @@ describe('Parrable ID System', function() { expect(bid.userId.parrableid).to.equal(P_COOKIE_VALUE); }); }); - utils.setCookie(P_COOKIE_NAME, '', EXPIRED_COOKIE_DATE); + storage.setCookie(P_COOKIE_NAME, '', EXPIRED_COOKIE_DATE); done(); }, { adUnits }); }); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index f717ef3a714..6ff2095665d 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -4,7 +4,8 @@ import { init, requestBidsHook, setSubmoduleRegistry, - syncDelay + syncDelay, + coreStorage } from 'modules/userId/index.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; @@ -67,27 +68,27 @@ describe('User ID', function() { } before(function() { - utils.setCookie('_pubcid_optout', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('_pubcid_optout', '', EXPIRED_COOKIE_DATE); localStorage.removeItem('_pbjs_id_optout'); localStorage.removeItem('_pubcid_optout'); }); describe('Decorate Ad Units', function() { beforeEach(function() { - utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('pubcid_alt', 'altpubcid200000', (new Date(Date.now() + 5000).toUTCString())); - sinon.spy(utils, 'setCookie'); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_alt', 'altpubcid200000', (new Date(Date.now() + 5000).toUTCString())); + sinon.spy(coreStorage, 'setCookie'); }); afterEach(function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); config.resetConfig(); - utils.setCookie.restore(); + coreStorage.setCookie.restore(); }); after(function() { - utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('pubcid_alt', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_alt', '', EXPIRED_COOKIE_DATE); }); it('Check same cookie behavior', function () { @@ -96,7 +97,7 @@ describe('User ID', function() { let innerAdUnits1; let innerAdUnits2; - let pubcid = utils.getCookie('pubcid'); + let pubcid = coreStorage.getCookie('pubcid'); expect(pubcid).to.be.null; // there should be no cookie initially setSubmoduleRegistry([pubCommonIdSubmodule]); @@ -104,7 +105,7 @@ describe('User ID', function() { config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); requestBidsHook(config => { innerAdUnits1 = config.adUnits }, {adUnits: adUnits1}); - pubcid = utils.getCookie('pubcid'); // cookies is created after requestbidHook + pubcid = coreStorage.getCookie('pubcid'); // cookies is created after requestbidHook innerAdUnits1.forEach(unit => { unit.bids.forEach(bid => { @@ -133,8 +134,8 @@ describe('User ID', function() { init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); requestBidsHook((config) => { innerAdUnits1 = config.adUnits }, {adUnits: adUnits1}); - pubcid1 = utils.getCookie('pubcid'); // get first cookie - utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); // erase cookie + pubcid1 = coreStorage.getCookie('pubcid'); // get first cookie + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); // erase cookie innerAdUnits1.forEach((unit) => { unit.bids.forEach((bid) => { @@ -152,7 +153,7 @@ describe('User ID', function() { config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); requestBidsHook((config) => { innerAdUnits2 = config.adUnits }, {adUnits: adUnits2}); - pubcid2 = utils.getCookie('pubcid'); // get second cookie + pubcid2 = coreStorage.getCookie('pubcid'); // get second cookie innerAdUnits2.forEach((unit) => { unit.bids.forEach((bid) => { @@ -187,7 +188,7 @@ describe('User ID', function() { }); }); // Because the cookie exists already, there should be no setCookie call by default - expect(utils.setCookie.callCount).to.equal(0); + expect(coreStorage.setCookie.callCount).to.equal(0); }); it('Extend cookie', function () { @@ -211,7 +212,7 @@ describe('User ID', function() { }); }); // Because extend is true, the cookie will be updated even if it exists already - expect(utils.setCookie.callCount).to.equal(1); + expect(coreStorage.setCookie.callCount).to.equal(1); }); it('Disable auto create', function () { @@ -230,13 +231,13 @@ describe('User ID', function() { expect(bid).to.not.have.deep.nested.property('userIdAsEids'); }); }); - expect(utils.setCookie.callCount).to.equal(0); + expect(coreStorage.setCookie.callCount).to.equal(0); }); }); describe('Opt out', function () { before(function () { - utils.setCookie('_pbjs_id_optout', '1', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('_pbjs_id_optout', '1', (new Date(Date.now() + 5000).toUTCString())); }); beforeEach(function () { @@ -245,14 +246,14 @@ describe('User ID', function() { afterEach(function () { // removed cookie - utils.setCookie('_pbjs_id_optout', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('_pbjs_id_optout', '', EXPIRED_COOKIE_DATE); $$PREBID_GLOBAL$$.requestBids.removeAll(); utils.logInfo.restore(); config.resetConfig(); }); after(function () { - utils.setCookie('_pbjs_id_optout', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('_pbjs_id_optout', '', EXPIRED_COOKIE_DATE); }); it('fails initialization if opt out cookie exists', function () { @@ -422,7 +423,7 @@ describe('User ID', function() { sandbox.stub(events, 'on'); // remove cookie - utils.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); adUnits = [getAdUnitMock()]; @@ -436,7 +437,7 @@ describe('User ID', function() { }; }, getId: function() { - const storedId = utils.getCookie('MOCKID'); + const storedId = coreStorage.getCookie('MOCKID'); if (storedId) { return {id: {'MOCKID': storedId}}; } @@ -593,7 +594,7 @@ describe('User ID', function() { }); it('does not delay auction if there are no ids to fetch', function() { - utils.setCookie('MOCKID', JSON.stringify({'MOCKID': '123456778'}), new Date(Date.now() + 5000).toUTCString()); + coreStorage.setCookie('MOCKID', JSON.stringify({'MOCKID': '123456778'}), new Date(Date.now() + 5000).toUTCString()); config.setConfig({ userSync: { @@ -624,7 +625,7 @@ describe('User ID', function() { }); it('test hook from pubcommonid cookie', function(done) { - utils.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 100000).toUTCString())); + coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 100000).toUTCString())); setSubmoduleRegistry([pubCommonIdSubmodule]); init(config); @@ -641,7 +642,7 @@ describe('User ID', function() { }); }); }); - utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); @@ -715,7 +716,7 @@ describe('User ID', function() { }); it('test hook from identityLink cookie', function(done) { - utils.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', (new Date(Date.now() + 100000).toUTCString())); + coreStorage.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', (new Date(Date.now() + 100000).toUTCString())); setSubmoduleRegistry([identityLinkSubmodule]); init(config); @@ -732,7 +733,7 @@ describe('User ID', function() { }); }); }); - utils.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); @@ -763,7 +764,7 @@ describe('User ID', function() { }); it('test hook from liveIntentId cookie', function(done) { - utils.setCookie('_li_pbid', JSON.stringify({'unifiedId': 'random-cookie-identifier'}), (new Date(Date.now() + 100000).toUTCString())); + coreStorage.setCookie('_li_pbid', JSON.stringify({'unifiedId': 'random-cookie-identifier'}), (new Date(Date.now() + 100000).toUTCString())); setSubmoduleRegistry([liveIntentIdSubmodule]); init(config); @@ -780,15 +781,15 @@ describe('User ID', function() { }); }); }); - utils.setCookie('_li_pbid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('_li_pbid', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); it('test hook from id5id cookies when refresh needed', function(done) { // simulate existing browser local storage values - utils.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('id5id_last', (new Date(Date.now() - 7200 * 1000)).toUTCString(), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('id5id_last', (new Date(Date.now() - 7200 * 1000)).toUTCString(), (new Date(Date.now() + 5000).toUTCString())); sinon.stub(utils, 'logError'); // getId should failed with a logError as it has no partnerId @@ -808,7 +809,7 @@ describe('User ID', function() { }); }); sinon.assert.calledOnce(utils.logError); - utils.setCookie('id5id', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('id5id', '', EXPIRED_COOKIE_DATE); utils.logError.restore(); done(); }, {adUnits}); @@ -862,7 +863,7 @@ describe('User ID', function() { }); it('test hook from liveIntentId cookie', function(done) { - utils.setCookie('_li_pbid', JSON.stringify({'unifiedId': 'random-cookie-identifier', 'segments': ['123']}), (new Date(Date.now() + 100000).toUTCString())); + coreStorage.setCookie('_li_pbid', JSON.stringify({'unifiedId': 'random-cookie-identifier', 'segments': ['123']}), (new Date(Date.now() + 100000).toUTCString())); setSubmoduleRegistry([liveIntentIdSubmodule]); init(config); @@ -881,14 +882,14 @@ describe('User ID', function() { }); }); }); - utils.setCookie('_li_pbid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('_li_pbid', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); it('test hook from britepoolid cookies', function(done) { // simulate existing browser local storage values - utils.setCookie('britepoolid', JSON.stringify({'primaryBPID': '279c0161-5152-487f-809e-05d7f7e653fd'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('britepoolid', JSON.stringify({'primaryBPID': '279c0161-5152-487f-809e-05d7f7e653fd'}), (new Date(Date.now() + 5000).toUTCString())); setSubmoduleRegistry([britepoolIdSubmodule]); init(config); @@ -905,14 +906,14 @@ describe('User ID', function() { }); }); }); - utils.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); it('test hook from netId cookies', function(done) { // simulate existing browser local storage values - utils.setCookie('netId', JSON.stringify({'netId': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('netId', JSON.stringify({'netId': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg'}), (new Date(Date.now() + 5000).toUTCString())); setSubmoduleRegistry([netIdSubmodule]); init(config); @@ -929,18 +930,18 @@ describe('User ID', function() { }); }); }); - utils.setCookie('netId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); it('test hook when pubCommonId, unifiedId, id5Id, identityLink, britepoolId and netId have data to pass', function(done) { - utils.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('unifiedid', JSON.stringify({'TDID': 'testunifiedid'}), (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'testunifiedid'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule]); init(config); @@ -975,23 +976,23 @@ describe('User ID', function() { expect(bid.userIdAsEids.length).to.equal(6); }); }); - utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('id5id', '', EXPIRED_COOKIE_DATE); - utils.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); - utils.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('netId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('id5id', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); it('test hook when pubCommonId, unifiedId, id5Id, britepoolId and netId have their modules added before and after init', function(done) { - utils.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); - utils.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); - utils.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); + coreStorage.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); + coreStorage.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); setSubmoduleRegistry([]); @@ -1038,24 +1039,24 @@ describe('User ID', function() { expect(bid.userIdAsEids.length).to.equal(6); }); }); - utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('id5id', '', EXPIRED_COOKIE_DATE); - utils.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); - utils.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('netId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('id5id', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); it('should add new id system ', function(done) { - utils.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); - utils.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); - utils.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); - utils.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), new Date(Date.now() + 5000).toUTCString()); - utils.setCookie('MOCKID', JSON.stringify({'MOCKID': '123456778'}), new Date(Date.now() + 5000).toUTCString()); + coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); + coreStorage.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); + coreStorage.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), new Date(Date.now() + 5000).toUTCString()); + coreStorage.setCookie('MOCKID', JSON.stringify({'MOCKID': '123456778'}), new Date(Date.now() + 5000).toUTCString()); setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule]); init(config); @@ -1122,13 +1123,13 @@ describe('User ID', function() { expect(bid.userIdAsEids.length).to.equal(6);// mid is unknown for eids.js }); }); - utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('id5id', '', EXPIRED_COOKIE_DATE); - utils.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); - utils.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('netId', '', EXPIRED_COOKIE_DATE); - utils.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('id5id', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); @@ -1138,17 +1139,17 @@ describe('User ID', function() { beforeEach(function() { sinon.stub(events, 'getEvents').returns([]); sinon.stub(utils, 'triggerPixel'); - utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('_parrable_eid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('_parrable_eid', '', EXPIRED_COOKIE_DATE); }); afterEach(function() { events.getEvents.restore(); utils.triggerPixel.restore(); - utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); - utils.setCookie('_parrable_eid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('_parrable_eid', '', EXPIRED_COOKIE_DATE); }); it('pubcid callback with url', function () { @@ -1204,7 +1205,7 @@ describe('User ID', function() { let innerAdUnits; const parrableStoredId = '01.1111111111.test-eid'; const parrableRefreshedId = '02.2222222222.test-eid'; - utils.setCookie('_parrable_eid', parrableStoredId, (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('_parrable_eid', parrableStoredId, (new Date(Date.now() + 5000).toUTCString())); const parrableIdSubmoduleMock = { name: 'parrableId', diff --git a/test/spec/modules/widespaceBidAdapter_spec.js b/test/spec/modules/widespaceBidAdapter_spec.js index c7d4f3bdaf9..95f0117e5d1 100644 --- a/test/spec/modules/widespaceBidAdapter_spec.js +++ b/test/spec/modules/widespaceBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec} from 'modules/widespaceBidAdapter.js'; +import {spec, storage} from 'modules/widespaceBidAdapter.js'; import includes from 'core-js/library/fn/array/includes.js'; describe('+widespaceAdatperTest', function () { @@ -141,8 +141,11 @@ describe('+widespaceAdatperTest', function () { }); describe('+bidRequest', function () { - const request = spec.buildRequests(bidRequest, bidderRequest); + let request; const UrlRegExp = /^((ftp|http|https):)?\/\/[^ "]+$/; + before(function() { + request = spec.buildRequests(bidRequest, bidderRequest); + }) let fakeLocalStorage = {}; let lsSetStub; @@ -150,15 +153,15 @@ describe('+widespaceAdatperTest', function () { let lsRemoveStub; beforeEach(function () { - lsSetStub = sinon.stub(window.localStorage, 'setItem').callsFake(function (name, value) { + lsSetStub = sinon.stub(storage, 'setDataInLocalStorage').callsFake(function (name, value) { fakeLocalStorage[name] = value; }); - lsGetStub = sinon.stub(window.localStorage, 'getItem').callsFake(function (key) { + lsGetStub = sinon.stub(storage, 'getDataFromLocalStorage').callsFake(function (key) { return fakeLocalStorage[key] || null; }); - lsRemoveStub = sinon.stub(window.localStorage, 'removeItem').callsFake(function (key) { + lsRemoveStub = sinon.stub(storage, 'removeDataFromLocalStorage').callsFake(function (key) { if (key && (fakeLocalStorage[key] !== null || fakeLocalStorage[key] !== undefined)) { delete fakeLocalStorage[key]; } diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 03635803b23..ccca9bdd0dc 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -1,4 +1,4 @@ -import { newBidder, registerBidder, preloadBidderMappingFile } from 'src/adapters/bidderFactory.js'; +import { newBidder, registerBidder, preloadBidderMappingFile, storage } from 'src/adapters/bidderFactory.js'; import adapterManager from 'src/adapterManager.js'; import * as ajax from 'src/ajax.js'; import { expect } from 'chai'; @@ -844,7 +844,7 @@ describe('preload mapping url hook', function() { beforeEach(function () { fakeTranslationServer = server; - getLocalStorageStub = sinon.stub(utils, 'getDataFromLocalStorage'); + getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); adapterManagerStub = sinon.stub(adapterManager, 'getBidAdapter'); }); diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js new file mode 100644 index 00000000000..0b406242f90 --- /dev/null +++ b/test/spec/unit/core/storageManager_spec.js @@ -0,0 +1,46 @@ +import { resetData, getCoreStorageManager, storageCallbacks, getStorageManager } from 'src/storageManager.js'; +import { config } from 'src/config.js'; +import * as utils from 'src/utils.js'; + +describe('storage manager', function() { + beforeEach(function() { + resetData(); + }); + + afterEach(function() { + config.resetConfig(); + }) + + it('should allow to set cookie for core modules without checking gdpr enforcements', function() { + const coreStorage = getCoreStorageManager(); + let date = new Date(); + date.setTime(date.getTime() + (24 * 60 * 60 * 1000)); + let expires = date.toUTCString(); + coreStorage.setCookie('hello', 'world', expires); + expect(coreStorage.getCookie('hello')).to.equal('world'); + }); + + it('should add done callbacks to storageCallbacks array', function() { + let noop = sinon.spy(); + const coreStorage = getStorageManager(); + + coreStorage.setCookie('foo', 'bar', null, null, null, noop); + coreStorage.getCookie('foo', noop); + coreStorage.localStorageIsEnabled(noop); + coreStorage.cookiesAreEnabled(noop); + coreStorage.setDataInLocalStorage('foo', 'bar', noop); + coreStorage.getDataFromLocalStorage('foo', noop); + coreStorage.removeDataFromLocalStorage('foo', noop); + coreStorage.hasLocalStorage(noop); + + expect(storageCallbacks.length).to.equal(8); + }); + + it('should allow bidder to access device if gdpr enforcement module is not included', function() { + let deviceAccessSpy = sinon.spy(utils, 'hasDeviceAccess'); + const storage = getStorageManager(); + storage.setCookie('foo1', 'baz1'); + expect(deviceAccessSpy.calledOnce).to.equal(true); + deviceAccessSpy.restore(); + }) +}); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index d98e7492e52..e7be6880444 100755 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -356,14 +356,6 @@ describe('Utils', function () { }); }); - describe('cookie functions', function() { - it('returns an array of cookies in a jar that have a similar name', function() { - utils.setCookie('cookie-a', 'cookie-value-a'); - utils.setCookie('cookie-b', 'cookie-value-b'); - expect(utils.findSimilarCookies('cookie')).to.include.members(['cookie-value-a', 'cookie-value-b']); - }); - }); - describe('isStr', function () { it('should return true with input string', function () { var output = utils.isStr(obj_string); From e25f358eb90f0a1518b463c2af459938ca59f5d4 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal <7393273+jaiminpanchal27@users.noreply.github.com> Date: Tue, 31 Mar 2020 19:34:10 -0400 Subject: [PATCH 38/90] Appnexus adapter: check purpose one (#5057) Co-authored-by: Jaimin Panchal --- modules/appnexusBidAdapter.js | 2 +- test/spec/modules/appnexusBidAdapter_spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index f6a5e1d3eb9..9c98ccc0651 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -412,7 +412,7 @@ function hasPurpose1Consent(bidderRequest) { let result = true; if (bidderRequest && bidderRequest.gdprConsent) { if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { - result = !!(utils.deepAccess(bidderRequest.gdprConsent, `vendorData.vendor.consents.${GVLID}`)); + result = !!(utils.deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); } } return result; diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 6febb8ecc9a..2a6a25752d9 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -788,9 +788,9 @@ describe('AppNexusAdapter', function () { gdprApplies: true, apiVersion: 2, vendorData: { - vendor: { + purpose: { consents: { - 32: false + 1: false } } } From 24e7fa3bfbacda62ceead686c28cf95a4b129cbb Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Tue, 31 Mar 2020 19:43:36 -0400 Subject: [PATCH 39/90] Prebid 3.14.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a05d8f367d1..93b05b886ac 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.14.0-pre", + "version": "3.14.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 6bfc210f4bfc23351fbc561724792d5066144f88 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Tue, 31 Mar 2020 20:03:19 -0400 Subject: [PATCH 40/90] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 93b05b886ac..7baae6a5f00 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.14.0", + "version": "3.15.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 6f4cf45c5663fc7ab304cc33f94e0f70f4b713b5 Mon Sep 17 00:00:00 2001 From: yieldlift <61510052+yieldlift@users.noreply.github.com> Date: Wed, 1 Apr 2020 10:02:09 +0200 Subject: [PATCH 41/90] YieldLift bid adapter (#5030) * yieldlift bid adapter * yielidlift bid adapter - removed 2 useless conditionals * support for pbjs3 - mediaTypes.banner.sizes Co-authored-by: yieldlift --- modules/yieldliftBidAdapter.js | 144 ++++++++++ modules/yieldliftBidAdapter.md | 31 +++ test/spec/modules/yieldliftBidAdapter_spec.js | 257 ++++++++++++++++++ 3 files changed, 432 insertions(+) create mode 100644 modules/yieldliftBidAdapter.js create mode 100644 modules/yieldliftBidAdapter.md create mode 100644 test/spec/modules/yieldliftBidAdapter_spec.js diff --git a/modules/yieldliftBidAdapter.js b/modules/yieldliftBidAdapter.js new file mode 100644 index 00000000000..87a598bd335 --- /dev/null +++ b/modules/yieldliftBidAdapter.js @@ -0,0 +1,144 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const ENDPOINT_URL = 'https://x.yieldlift.com/auction'; + +const DEFAULT_BID_TTL = 30; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; + +export const spec = { + code: 'yieldlift', + aliases: ['yl'], + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return (!!bid.params.unitId && typeof bid.params.unitId === 'string') || + (!!bid.params.networkId && typeof bid.params.networkId === 'string') || + (!!bid.params.publisherId && typeof bid.params.publisherId === 'string'); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + if (!validBidRequests || !bidderRequest) { + return; + } + const publisherId = validBidRequests[0].params.publisherId; + const networkId = validBidRequests[0].params.networkId; + const impressions = validBidRequests.map(bidRequest => ({ + id: bidRequest.bidId, + banner: { + format: bidRequest.mediaTypes.banner.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1] + })) + }, + ext: { + exchange: { + unitId: bidRequest.params.unitId + } + } + })); + + const openrtbRequest = { + id: bidderRequest.auctionId, + imp: impressions, + site: { + domain: window.location.hostname, + page: window.location.href, + ref: bidderRequest.refererInfo ? bidderRequest.refererInfo.referer || null : null + }, + ext: { + exchange: { + publisherId: publisherId, + networkId: networkId, + } + } + }; + + // adding schain object + if (validBidRequests[0].schain) { + utils.deepSetValue(openrtbRequest, 'source.ext.schain', validBidRequests[0].schain); + } + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + utils.deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + utils.deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + utils.deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + const payloadString = JSON.stringify(openrtbRequest); + return { + method: 'POST', + url: ENDPOINT_URL, + data: payloadString, + }; + }, + + interpretResponse: function (serverResponse) { + const bidResponses = []; + const response = (serverResponse || {}).body; + // response is always one seat (exchange) with (optional) bids for each impression + if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) { + response.seatbid[0].bid.forEach(bid => { + bidResponses.push({ + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + ad: bid.adm, + ttl: DEFAULT_BID_TTL, + creativeId: bid.crid, + netRevenue: DEFAULT_NET_REVENUE, + currency: DEFAULT_CURRENCY, + }) + }) + } else { + utils.logInfo('yieldlift.interpretResponse :: no valid responses to interpret'); + } + return bidResponses; + }, + getUserSyncs: function (syncOptions, serverResponses) { + utils.logInfo('yieldlift.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses); + let syncs = []; + + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + return syncs; + } + + serverResponses.forEach(resp => { + const userSync = utils.deepAccess(resp, 'body.ext.usersync'); + if (userSync) { + let syncDetails = []; + Object.keys(userSync).forEach(key => { + const value = userSync[key]; + if (value.syncs && value.syncs.length) { + syncDetails = syncDetails.concat(value.syncs); + } + }); + syncDetails.forEach(syncDetails => { + syncs.push({ + type: syncDetails.type === 'iframe' ? 'iframe' : 'image', + url: syncDetails.url + }); + }); + + if (!syncOptions.iframeEnabled) { + syncs = syncs.filter(s => s.type !== 'iframe') + } + if (!syncOptions.pixelEnabled) { + syncs = syncs.filter(s => s.type !== 'image') + } + } + }); + utils.logInfo('yieldlift.getUserSyncs result=%o', syncs); + return syncs; + }, + +}; +registerBidder(spec); diff --git a/modules/yieldliftBidAdapter.md b/modules/yieldliftBidAdapter.md new file mode 100644 index 00000000000..96b5de1b618 --- /dev/null +++ b/modules/yieldliftBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +``` +Module Name: YieldLift Bid Adapter +Module Type: Bidder Adapter +Maintainer: info@yieldlift.com +``` + +# Description + +Module that connects to YieldLift's demand sources + +# Test Parameters +``` +var adUnits = [ + { + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'yieldlift', + params: { + unitId: 'test' + } + }] + } +]; +``` diff --git a/test/spec/modules/yieldliftBidAdapter_spec.js b/test/spec/modules/yieldliftBidAdapter_spec.js new file mode 100644 index 00000000000..640a0f4e613 --- /dev/null +++ b/test/spec/modules/yieldliftBidAdapter_spec.js @@ -0,0 +1,257 @@ +import {expect} from 'chai'; +import {spec} from 'modules/yieldliftBidAdapter.js'; + +const REQUEST = { + 'bidderCode': 'yieldlift', + 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708', + 'bidderRequestId': 'requestId', + 'bidRequest': [{ + 'bidder': 'yieldlift', + 'params': { + 'unitId': 123456, + }, + 'placementCode': 'div-gpt-dummy-placement-code', + 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, + 'bidId': 'bidId1', + 'bidderRequestId': 'bidderRequestId', + 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708' + }, + { + 'bidder': 'yieldlift', + 'params': { + 'unitId': 123456, + }, + 'placementCode': 'div-gpt-dummy-placement-code', + 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, + 'bidId': 'bidId2', + 'bidderRequestId': 'bidderRequestId', + 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708' + }], + 'start': 1487883186070, + 'auctionStart': 1487883186069, + 'timeout': 3000 +}; + +const RESPONSE = { + 'headers': null, + 'body': { + 'id': 'responseId', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'bidId1', + 'impid': 'bidId1', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 334553, + 'auction_id': 514667951122925701, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + }, + { + 'id': 'bidId2', + 'impid': 'bidId2', + 'price': 0.1, + 'adm': '', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 386046, + 'auction_id': 517067951122925501, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + } + ], + 'seat': 'yieldlift' + } + ], + 'ext': { + 'usersync': { + 'sovrn': { + 'status': 'none', + 'syncs': [ + { + 'url': 'urlsovrn', + 'type': 'iframe' + } + ] + }, + 'appnexus': { + 'status': 'none', + 'syncs': [ + { + 'url': 'urlappnexus', + 'type': 'pixel' + } + ] + } + }, + 'responsetimemillis': { + 'appnexus': 127 + } + } + } +}; + +describe('YieldLift', function () { + describe('isBidRequestValid', function () { + it('should accept request if only unitId is passed', function () { + let bid = { + bidder: 'yieldlift', + params: { + unitId: 'unitId', + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should accept request if only networkId is passed', function () { + let bid = { + bidder: 'yieldlift', + params: { + networkId: 'networkId', + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should accept request if only publisherId is passed', function () { + let bid = { + bidder: 'yieldlift', + params: { + publisherId: 'publisherId', + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('reject requests without params', function () { + let bid = { + bidder: 'yieldlift', + params: {} + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('creates request data', function () { + let request = spec.buildRequests(REQUEST.bidRequest, REQUEST); + + expect(request).to.exist.and.to.be.a('object'); + const payload = JSON.parse(request.data); + expect(payload.imp[0]).to.have.property('id', REQUEST.bidRequest[0].bidId); + expect(payload.imp[1]).to.have.property('id', REQUEST.bidRequest[1].bidId); + }); + + it('has gdpr data if applicable', function () { + const req = Object.assign({}, REQUEST, { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, + } + }); + let request = spec.buildRequests(REQUEST.bidRequest, req); + + const payload = JSON.parse(request.data); + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + }); + + describe('interpretResponse', function () { + it('have bids', function () { + let bids = spec.interpretResponse(RESPONSE, REQUEST); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + validateBidOnIndex(1); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].impid); + expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); + expect(bids[index]).to.have.property('ttl', 30); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, REQUEST); + + expect(bids).to.be.empty; + }); + }); + + describe('getUserSyncs', function () { + it('handles no parameters', function () { + let opts = spec.getUserSyncs({}); + expect(opts).to.be.an('array').that.is.empty; + }); + it('returns non if sync is not allowed', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + + expect(opts).to.be.an('array').that.is.empty; + }); + + it('iframe sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [RESPONSE]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['sovrn'].syncs[0].url); + }); + + it('pixel sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['appnexus'].syncs[0].url); + }); + + it('all sync enabled should return all results', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]); + + expect(opts.length).to.equal(2); + }); + }); +}); From a07ae78e64ebc0da2a6b20a8ae205e02df41a663 Mon Sep 17 00:00:00 2001 From: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Date: Wed, 1 Apr 2020 11:04:27 +0300 Subject: [PATCH 42/90] Added TheMediaGridNM Bid Adapter (#4992) * Added TheMediaGridNM Bid Adapter * Updated required params for TheMediaGridNM Bid Adapter * Update TheMediGridNM Bid Adapter * Fix tests for TheMediaGridNM Bid Adapter * Fixes after review for TheMediaGridNM Bid Adapter --- modules/gridNMBidAdapter.js | 234 +++++++++++ modules/gridNMBidAdapter.md | 39 ++ test/spec/modules/gridNMBidAdapter_spec.js | 461 +++++++++++++++++++++ 3 files changed, 734 insertions(+) create mode 100644 modules/gridNMBidAdapter.js create mode 100644 modules/gridNMBidAdapter.md create mode 100644 test/spec/modules/gridNMBidAdapter_spec.js diff --git a/modules/gridNMBidAdapter.js b/modules/gridNMBidAdapter.js new file mode 100644 index 00000000000..7e244f8293c --- /dev/null +++ b/modules/gridNMBidAdapter.js @@ -0,0 +1,234 @@ +import * as utils from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { Renderer } from '../src/Renderer.js'; +import { VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'gridNM'; +const ENDPOINT_URL = 'https://grid.bidswitch.net/hbnm'; +const SYNC_URL = 'https://x.bidswitch.net/sync?ssp=iponweblabs'; +const TIME_TO_LIVE = 360; +const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + +let hasSynced = false; + +const LOG_ERROR_MESS = { + noAdm: 'Bid from response has no adm parameter - ', + noPrice: 'Bid from response has no price parameter - ', + wrongContentType: 'Bid from response has wrong content_type parameter - ', + noBid: 'Array of bid objects is empty', + noPlacementCode: 'Can\'t find in requested bids the bid with auid - ', + emptyUids: 'Uids should be not empty', + emptySeatbid: 'Seatbid array from response has empty item', + emptyResponse: 'Response is empty', + hasEmptySeatbidArray: 'Response has empty seatbid array', + hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [ VIDEO ], + /** + * 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. + */ + isBidRequestValid: function(bid) { + let invalid = + !bid.params.source || !utils.isStr(bid.params.source) || + !bid.params.secid || !utils.isStr(bid.params.secid) || + !bid.params.pubid || !utils.isStr(bid.params.pubid); + + if (!invalid) { + invalid = !bid.params.video || !bid.params.video.protocols || !bid.params.video.mimes; + } + if (!invalid) { + const {protocols, mimes} = bid.params.video; + invalid = !utils.isArray(mimes) || !mimes.length || mimes.filter((it) => !(it && utils.isStr(it))).length; + if (!invalid) { + invalid = !utils.isArray(protocols) || !protocols.length || protocols.filter((it) => !(utils.isNumber(it) && it > 0 && !(it % 1))).length; + } + } + return !invalid; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - an array of bids + * @param {bidderRequest} bidderRequest bidder request object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const bids = validBidRequests || []; + const requests = []; + + bids.forEach(bid => { + const {params, bidderRequestId, sizes} = bid; + const payload = { + sizes: utils.parseSizesInput(sizes).join(','), + r: bidderRequestId, + wrapperType: 'Prebid_js', + wrapperVersion: '$prebid.version$' + }; + + if (bidderRequest) { + if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { + payload.u = bidderRequest.refererInfo.referer; + } + if (bidderRequest.timeout) { + payload.wtimeout = bidderRequest.timeout; + } + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + payload.gdpr_applies = + (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') + ? Number(bidderRequest.gdprConsent.gdprApplies) : 1; + } + if (bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } + } + + requests.push({ + method: 'POST', + url: ENDPOINT_URL + '?' + utils.parseQueryStringParameters(payload).replace(/\&$/, ''), + bid: bid, + data: params // content + }); + }); + + return requests; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @param {*} bidRequest + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + serverResponse = serverResponse && serverResponse.body; + const bidResponses = []; + + let errorMessage; + + if (!serverResponse) errorMessage = LOG_ERROR_MESS.emptyResponse; + else if (serverResponse.seatbid && !serverResponse.seatbid.length) { + errorMessage = LOG_ERROR_MESS.hasEmptySeatbidArray; + } + + if (!errorMessage && serverResponse.seatbid) { + const serverBid = _getBidFromResponse(serverResponse.seatbid[0]); + if (serverBid) { + if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); + else if (!serverBid.price) errorMessage = LOG_ERROR_MESS.noPrice + JSON.stringify(serverBid); + else if (serverBid.content_type !== 'video') errorMessage = LOG_ERROR_MESS.wrongContentType + serverBid.content_type; + if (!errorMessage) { + const bid = bidRequest.bid; + if (!serverBid.w || !serverBid.h) { + const size = utils.parseSizesInput(bid.sizes)[0].split('x'); + serverBid.w = size[0]; + serverBid.h = size[1]; + } + const bidResponse = { + requestId: bid.bidId, + bidderCode: spec.code, + cpm: serverBid.price, + width: serverBid.w, + height: serverBid.h, + creativeId: serverBid.auid || bid.bidderRequestId, + currency: 'USD', + netRevenue: false, + ttl: TIME_TO_LIVE, + dealId: serverBid.dealid, + vastXml: serverBid.adm, + mediaType: VIDEO, + adResponse: { + content: serverBid.adm + } + }; + + if (!bid.renderer && (!bid.mediaTypes || !bid.mediaTypes.video || bid.mediaTypes.video.context === 'outstream')) { + bidResponse.renderer = createRenderer(bidResponse, { + id: bid.bidId, + url: RENDERER_URL + }); + } + bidResponses.push(bidResponse); + } + } + } + if (errorMessage) utils.logError(errorMessage); + return bidResponses; + }, + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + if (!hasSynced && syncOptions.pixelEnabled) { + let params = ''; + + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + params += `&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent) { + params += `&us_privacy=${uspConsent}`; + } + + hasSynced = true; + return { + type: 'image', + url: SYNC_URL + params + }; + } + } +}; + +function _getBidFromResponse(respItem) { + if (!respItem) { + utils.logError(LOG_ERROR_MESS.emptySeatbid); + } else if (!respItem.bid) { + utils.logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); + } else if (!respItem.bid[0]) { + utils.logError(LOG_ERROR_MESS.noBid); + } + return respItem && respItem.bid && respItem.bid[0]; +} + +function outstreamRender (bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: bid.adResponse + }); + }); +} + +function createRenderer (bid, rendererParams) { + const renderer = Renderer.install({ + id: rendererParams.id, + url: rendererParams.url, + loaded: false + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; +} + +export function resetUserSync() { + hasSynced = false; +} + +export function getSyncUrl() { + return SYNC_URL; +} + +registerBidder(spec); diff --git a/modules/gridNMBidAdapter.md b/modules/gridNMBidAdapter.md new file mode 100644 index 00000000000..6decdde7f4c --- /dev/null +++ b/modules/gridNMBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +Module Name: The Grid Media Bidder Adapter +Module Type: Bidder Adapter +Maintainer: grid-tech@themediagrid.com + +# Description + +Module that connects to Grid demand source to fetch bids. +Grid bid adapter supports Banner and Video (instream and outstream). + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + playerSize: [728, 90], + context: 'outstream' + } + }, + bids: [ + { + bidder: "gridNM", + params: { + source: 'jwp', + secid: '11', + pubid: '22', + video: { + mimes: ['video/mp4', 'video/x-ms-wmv'], + protocols: [1,2,3,4,5,6] + } + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/gridNMBidAdapter_spec.js b/test/spec/modules/gridNMBidAdapter_spec.js new file mode 100644 index 00000000000..0dbaac0c526 --- /dev/null +++ b/test/spec/modules/gridNMBidAdapter_spec.js @@ -0,0 +1,461 @@ +import { expect } from 'chai'; +import { spec, resetUserSync, getSyncUrl } from 'modules/gridNMBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('TheMediaGridNM Adapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + const paramsList = [ + { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + } + }, + { + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + { + 'source': 'jwp', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + { + 'source': 'jwp', + 'secid': '11', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + } + ]; + paramsList.forEach((params) => { + const invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = params; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + it('should return false when required params has invalid values', function () { + const paramsList = [ + { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': '1,2,3,4,5' + } + }, + { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': [1, 2], + 'protocols': [1, 2, 3, 4, 5] + } + }, + { + 'source': 'jwp', + 'secid': 11, + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5] + } + }, + { + 'source': 111, + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5] + } + } + ]; + + paramsList.forEach((params) => { + const invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = params; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + }); + + describe('buildRequests', function () { + function parseRequestUrl(url) { + const res = {}; + url.replace(/^[^\?]+\?/, '').split('&').forEach((it) => { + const couple = it.split('='); + res[couple[0]] = decodeURIComponent(couple[1]); + }); + return res; + } + const bidderRequest = {refererInfo: {referer: 'https://example.com'}}; + const referrer = bidderRequest.refererInfo.referer; + let bidRequests = [ + { + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3], + 'skip': 1 + } + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '3150ccb55da321', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('should attach valid params to the tag', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + const requestsSizes = ['300x250,300x600', '728x90']; + requests.forEach((req, i) => { + expect(req.url).to.be.an('string'); + const payload = parseRequestUrl(req.url); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('r', '22edbae2733bf6'); + expect(payload).to.have.property('wrapperType', 'Prebid_js'); + expect(payload).to.have.property('wrapperVersion', '$prebid.version$'); + expect(payload).to.have.property('sizes', requestsSizes[i]); + expect(req.data).to.deep.equal(bidRequests[i].params); + }); + }); + + it('if gdprConsent is present payload must have gdpr params', function () { + const [request] = spec.buildRequests([bidRequests[0]], {gdprConsent: {consentString: 'AAA', gdprApplies: true}, refererInfo: bidderRequest.refererInfo}); + expect(request.url).to.be.an('string'); + const payload = parseRequestUrl(request.url); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '1'); + }); + + it('if gdprApplies is false gdpr_applies must be 0', function () { + const [request] = spec.buildRequests([bidRequests[0]], {gdprConsent: {consentString: 'AAA', gdprApplies: false}}); + expect(request.url).to.be.an('string'); + const payload = parseRequestUrl(request.url); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '0'); + }); + + it('if gdprApplies is undefined gdpr_applies must be 1', function () { + const [request] = spec.buildRequests([bidRequests[0]], {gdprConsent: {consentString: 'AAA'}}); + expect(request.url).to.be.an('string'); + const payload = parseRequestUrl(request.url); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '1'); + }); + + it('if usPrivacy is present payload must have us_privacy param', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const [request] = spec.buildRequests([bidRequests[0]], bidderRequestWithUSP); + expect(request.url).to.be.an('string'); + const payload = parseRequestUrl(request.url); + expect(payload).to.have.property('us_privacy', '1YNN'); + }); + }); + + describe('interpretResponse', function () { + const responses = [ + {'bid': [{'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'content_type': 'video', 'h': 250, 'w': 300, 'dealid': 11}], 'seat': '2'}, + {'bid': [{'price': 0.5, 'adm': '\n<\/Ad>\n<\/VAST>', 'content_type': 'video', 'h': 600, 'w': 300}], 'seat': '2'}, + {'bid': [{'price': 0, 'h': 250, 'w': 300}], 'seat': '2'}, + {'bid': [{'price': 0, 'adm': '\n<\/Ad>\n<\/VAST>', 'h': 250, 'w': 300}], 'seat': '2'}, + undefined, + {'bid': [], 'seat': '2'}, + {'seat': '2'}, + ]; + + it('should get correct video bid response', function () { + const bidRequests = [ + { + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '659423fff799cb', + 'bidderRequestId': '5f2009617a7c0a', + 'auctionId': '1cbd2feafe5e8b', + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + }, + { + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5], + 'skip': 1 + } + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '2bc598e42b6a', + 'bidderRequestId': '1e8b5a465f404', + 'auctionId': '1cbd2feafe5e8b', + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + } + ]; + const requests = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '659423fff799cb', + 'cpm': 1.15, + 'creativeId': '5f2009617a7c0a', + 'dealId': 11, + 'width': 300, + 'height': 250, + 'bidderCode': 'gridNM', + 'currency': 'USD', + 'mediaType': 'video', + 'netRevenue': false, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + } + }, + { + 'requestId': '2bc598e42b6a', + 'cpm': 0.5, + 'creativeId': '1e8b5a465f404', + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'bidderCode': 'gridNM', + 'currency': 'USD', + 'mediaType': 'video', + 'netRevenue': false, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + } + } + ]; + + requests.forEach((req, i) => { + const result = spec.interpretResponse({'body': {'seatbid': [responses[i]]}}, req); + expect(result[0]).to.deep.equal(expectedResponse[i]); + }); + }); + + it('handles wrong and nobid responses', function () { + responses.slice(2).forEach((resp) => { + const request = spec.buildRequests([{ + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5], + 'skip': 1 + } + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '2bc598e42b6a', + 'bidderRequestId': '39d74f5b71464', + 'auctionId': '1cbd2feafe5e8b', + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + }]); + const result = spec.interpretResponse({'body': {'seatbid': [resp]}}, request[0]); + expect(result.length).to.equal(0); + }); + }); + }); + + describe('user sync', function () { + const syncUrl = getSyncUrl(); + + beforeEach(function () { + resetUserSync(); + }); + + it('should register the Emily iframe', function () { + let syncs = spec.getUserSyncs({ + pixelEnabled: true + }); + + expect(syncs).to.deep.equal({type: 'image', url: syncUrl}); + }); + + it('should not register the Emily iframe more than once', function () { + let syncs = spec.getUserSyncs({ + pixelEnabled: true + }); + expect(syncs).to.deep.equal({type: 'image', url: syncUrl}); + + // when called again, should still have only been called once + syncs = spec.getUserSyncs(); + expect(syncs).to.equal(undefined); + }); + + it('should pass gdpr params if consent is true', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + gdprApplies: true, consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr=1&gdpr_consent=foo` + }); + }); + + it('should pass gdpr params if consent is false', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + gdprApplies: false, consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr=0&gdpr_consent=foo` + }); + }); + + it('should pass gdpr param gdpr_consent only when gdprApplies is undefined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr_consent=foo` + }); + }); + + it('should pass no params if gdpr consentString is not defined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {})).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is a number', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: 0 + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is null', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: null + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is a object', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: {} + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr is not defined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined)).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass usPrivacy param if it is available', function() { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {}, '1YNN')).to.deep.equal({ + type: 'image', url: `${syncUrl}&us_privacy=1YNN` + }); + }); + }); +}); From 83a362cb7ab63aefba593d554a8e4fd2d4dd1384 Mon Sep 17 00:00:00 2001 From: Nepomuk Seiler Date: Wed, 1 Apr 2020 15:22:59 +0200 Subject: [PATCH 43/90] Add type information to DSPX bidResponse (#5021) * Add type information to DSPX bidResponse Depending on the ad type other ad placements can be blocked or not. For example a Skin Ad may require that all skys are blocked so they don't overlap. For a regular display banner this isn't necessary. * Add type to unit test * Revert "Add type information to DSPX bidResponse" This reverts commit 9403ceac4f636c6c3f8576dbfb18ef6d1b93ff16. * Add type information to DSPX bidResponse Depending on the ad type other ad placements can be blocked or not. For example a Skin Ad may require that all skys are blocked so they don't overlap. For a regular display banner this isn't necessary. * Revert "Add type to unit test" This reverts commit c08ce996a2317adb968033920770e0af1a80efa6. * Add type to unit test --- modules/dspxBidAdapter.js | 1 + test/spec/modules/dspxBidAdapter_spec.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/modules/dspxBidAdapter.js b/modules/dspxBidAdapter.js index 0bde2b6ec10..16764e76ede 100644 --- a/modules/dspxBidAdapter.js +++ b/modules/dspxBidAdapter.js @@ -74,6 +74,7 @@ export const spec = { dealId: dealId, currency: currency, netRevenue: netRevenue, + type: response.type, ttl: config.getConfig('_bidderTimeout'), ad: response.adTag }; diff --git a/test/spec/modules/dspxBidAdapter_spec.js b/test/spec/modules/dspxBidAdapter_spec.js index 1156eb8d169..2513a6174cd 100644 --- a/test/spec/modules/dspxBidAdapter_spec.js +++ b/test/spec/modules/dspxBidAdapter_spec.js @@ -108,6 +108,7 @@ describe('dspxAdapter', function () { 'crid': 100500, 'width': '300', 'height': '250', + 'type': 'sspHTML', 'tag': '', 'requestId': '220ed41385952a', 'currency': 'EUR', @@ -127,6 +128,7 @@ describe('dspxAdapter', function () { currency: 'EUR', netRevenue: true, ttl: 300, + type: 'sspHTML', ad: '' }]; From 5af6c6428572d0693baf59206d09636cbc63c579 Mon Sep 17 00:00:00 2001 From: MIGOdanis Date: Wed, 1 Apr 2020 22:39:31 +0800 Subject: [PATCH 44/90] change clickforce host url (#5061) --- modules/clickforceBidAdapter.js | 6 +++--- test/spec/modules/clickforceBidAdapter_spec.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/clickforceBidAdapter.js b/modules/clickforceBidAdapter.js index eb73033e2d0..20408fe9177 100644 --- a/modules/clickforceBidAdapter.js +++ b/modules/clickforceBidAdapter.js @@ -2,7 +2,7 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; const BIDDER_CODE = 'clickforce'; -const ENDPOINT_URL = 'https://ad.doublemax.net/adserver/prebid.json?cb=' + new Date().getTime() + '&hb=1&ver=1.21'; +const ENDPOINT_URL = 'https://ad.holmesmind.com/adserver/prebid.json?cb=' + new Date().getTime() + '&hb=1&ver=1.21'; export const spec = { code: BIDDER_CODE, @@ -112,12 +112,12 @@ export const spec = { if (syncOptions.iframeEnabled) { return [{ type: 'iframe', - url: 'https://cdn.doublemax.net/js/capmapping.htm' + url: 'https://cdn.holmesmind.com/js/capmapping.htm' }] } else if (syncOptions.pixelEnabled) { return [{ type: 'image', - url: 'https://c.doublemax.net/cm' + url: 'https://c.holmesmind.com/cm' }] } } diff --git a/test/spec/modules/clickforceBidAdapter_spec.js b/test/spec/modules/clickforceBidAdapter_spec.js index d0fb9439534..dad00f94641 100644 --- a/test/spec/modules/clickforceBidAdapter_spec.js +++ b/test/spec/modules/clickforceBidAdapter_spec.js @@ -175,7 +175,7 @@ describe('ClickforceAdapter', function () { } let userSync = spec.getUserSyncs(syncOptions); expect(userSync[0].type).to.equal('iframe'); - expect(userSync[0].url).to.equal('https://cdn.doublemax.net/js/capmapping.htm'); + expect(userSync[0].url).to.equal('https://cdn.holmesmind.com/js/capmapping.htm'); }); it('should register type is image', function () { @@ -184,7 +184,7 @@ describe('ClickforceAdapter', function () { } let userSync = spec.getUserSyncs(syncOptions); expect(userSync[0].type).to.equal('image'); - expect(userSync[0].url).to.equal('https://c.doublemax.net/cm'); + expect(userSync[0].url).to.equal('https://c.holmesmind.com/cm'); }); }); }); From 37352f36c4f2b53abc18874f1eebd94088a2dc65 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Wed, 1 Apr 2020 12:31:21 -0700 Subject: [PATCH 45/90] Eids uid string only (#5047) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * the user-id in eid should be string only * updated conversant bidder spec for eid-uid to be string --- modules/userId/eids.js | 2 +- .../spec/modules/conversantBidAdapter_spec.js | 6 ++--- test/spec/modules/eids_spec.js | 22 ++++++++++++++----- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/modules/userId/eids.js b/modules/userId/eids.js index bd3b9bac0ff..5ca9e40866b 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -91,7 +91,7 @@ function createEidObject(userIdData, subModuleKey) { let eid = {}; eid.source = conf['source']; const value = utils.isFn(conf['getValue']) ? conf['getValue'](userIdData) : userIdData; - if (value) { + if (utils.isStr(value)) { const uid = { id: value, atype: conf['atype'] }; // getUidExt if (utils.isFn(conf['getUidExt'])) { diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index 3d6a6be9fa1..28075628d5a 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -470,14 +470,14 @@ describe('Conversant adapter tests', function() { // add pubcid to every entry requests.forEach((unit) => { - Object.assign(unit, {userId: {pubcid: 112233, tdid: 223344, idl_env: 334455}}); + Object.assign(unit, {userId: {pubcid: '112233', tdid: '223344', idl_env: '334455'}}); Object.assign(unit, {userIdAsEids: createEidsArray(unit.userId)}); }); // construct http post payload const payload = spec.buildRequests(requests).data; expect(payload).to.have.deep.nested.property('user.ext.eids', [ - {source: 'adserver.org', uids: [{id: 223344, atype: 1, ext: {rtiPartner: 'TDID'}}]}, - {source: 'liveramp.com', uids: [{id: 334455, atype: 1}]} + {source: 'adserver.org', uids: [{id: '223344', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'liveramp.com', uids: [{id: '334455', atype: 1}]} ]); }); }); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 99c34d28a94..ed32ecc51d2 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -1,8 +1,8 @@ import {createEidsArray} from 'modules/userId/eids.js'; import {expect} from 'chai'; -// Note: In unit tets cases for bidders, call the createEidsArray function over userId object that is used for calling fetchBids -// this way the request will stay consistent and unit test cases will not need lots of changes. +// Note: In unit tets cases for bidders, call the createEidsArray function over userId object that is used for calling fetchBids +// this way the request will stay consistent and unit test cases will not need lots of changes. describe('eids array generation for known sub-modules', function() { it('pubCommonId', function() { @@ -158,12 +158,24 @@ describe('Negative case', function() { expect(newEids.length).to.equal(0); }); - it('eids array generation for known sub-module with undefined value', function() { + it('eids array generation for known sub-module with non-string value', function() { // pubCommonId - const userId = { + let userId = { pubcid: undefined }; - const newEids = createEidsArray(userId); + let newEids = createEidsArray(userId); + expect(newEids.length).to.equal(0); + userId.pubcid = 123; + newEids = createEidsArray(userId); + expect(newEids.length).to.equal(0); + userId.pubcid = []; + newEids = createEidsArray(userId); + expect(newEids.length).to.equal(0); + userId.pubcid = {}; + newEids = createEidsArray(userId); + expect(newEids.length).to.equal(0); + userId.pubcid = null; + newEids = createEidsArray(userId); expect(newEids.length).to.equal(0); }); }); From d88e8081820aa01321403aca9998e421e357a411 Mon Sep 17 00:00:00 2001 From: Steve Alliance Date: Wed, 1 Apr 2020 16:22:55 -0400 Subject: [PATCH 46/90] Districtm Dmx adding Coppa support (#4997) * idsync support ccpa & gdpr * adding support for COPPA * using proposition to change COPPA setting in dmx adapter * adding support for liveramp * update md file --- modules/districtmDMXBidAdapter.js | 25 ++++++++ modules/districtmDmxBidAdapter.md | 9 ++- .../modules/districtmDmxBidAdapter_spec.js | 57 ++++++++++++++++++- 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/modules/districtmDMXBidAdapter.js b/modules/districtmDMXBidAdapter.js index 83f3ac5b3c9..0dbaf2b2336 100644 --- a/modules/districtmDMXBidAdapter.js +++ b/modules/districtmDMXBidAdapter.js @@ -72,6 +72,7 @@ export const spec = { publisher: { id: String(bidRequest[0].params.memberid) || null } } } + try { let params = config.getConfig('dmx'); dmxRequest.user = params.user || {}; @@ -80,6 +81,14 @@ export const spec = { } catch (e) { } + + let eids = []; + if (bidRequest && bidRequest.userId) { + bindUserId(eids, utils.deepAccess(bidRequest, `userId.idl_env`), 'liveramp.com', 1); + dmxRequest.user = dmxRequest.user || {}; + dmxRequest.user.ext = dmxRequest.user.ext || {}; + dmxRequest.user.ext.eids = eids; + } if (!dmxRequest.test) { delete dmxRequest.test; } @@ -91,6 +100,8 @@ export const spec = { dmxRequest.user.ext = {}; dmxRequest.user.ext.consent = bidderRequest.gdprConsent.consentString; } + dmxRequest.regs = dmxRequest.regs || {}; + dmxRequest.regs.coppa = config.getConfig('coppa') === true ? 1 : 0; if (bidderRequest && bidderRequest.uspConsent) { dmxRequest.regs = dmxRequest.regs || {}; dmxRequest.regs.ext = dmxRequest.regs.ext || {}; @@ -281,4 +292,18 @@ export function defaultSize(thebidObj) { returnObject.height = checkDeepArray(sizes)[1]; return returnObject; } + +export function bindUserId(eids, value, source, atype) { + if (utils.isStr(value) && Array.isArray(eids)) { + eids.push({ + source, + uids: [ + { + id: value, + atype + } + ] + }) + } +} registerBidder(spec); diff --git a/modules/districtmDmxBidAdapter.md b/modules/districtmDmxBidAdapter.md index 5bc1e5d5780..792cf2e7305 100644 --- a/modules/districtmDmxBidAdapter.md +++ b/modules/districtmDmxBidAdapter.md @@ -13,6 +13,8 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman * Single Request * Multi-Size Support * GDPR Compliant +* CCPA Compliant +* COPPA Compliant * Bids returned in **NET** ## Media Types @@ -23,8 +25,8 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman | Key | Scope | Type | Description | --- | --- | --- | --- -| dmxid | Mandatory | Integer | Unique identifier of the placement, dmxid can be obtained in the district m Boost platform. -| memberid | Mandatory | Integer | Unique identifier for your account, memberid can be obtained in the district m Boost platform. +| `dmxid` | Mandatory | Integer | Unique identifier of the placement, dmxid can be obtained in the district m Boost platform. +| `memberid` | Mandatory | Integer | Unique identifier for your account, memberid can be obtained in the district m Boost platform. # Ad Unit Configuration Example @@ -47,6 +49,9 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman ``` +# Ad Unit Configuration when COPPA is needed + + # Quick Start Guide ###### 1. Including the `districtmDmxAdapter` in your build process. diff --git a/test/spec/modules/districtmDmxBidAdapter_spec.js b/test/spec/modules/districtmDmxBidAdapter_spec.js index e7a8e8c3064..d8f0beb9a36 100644 --- a/test/spec/modules/districtmDmxBidAdapter_spec.js +++ b/test/spec/modules/districtmDmxBidAdapter_spec.js @@ -40,7 +40,7 @@ const bidRequest = [{ 'bidder': 'districtmDMX', 'params': { 'dmxid': 100001, - 'memberid': 100003 + 'memberid': 100003, }, 'adUnitCode': 'div-gpt-ad-12345678-1', 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', @@ -53,6 +53,22 @@ const bidRequest = [{ 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' }]; +const bidRequestNoCoppa = [{ + 'bidder': 'districtmDMX', + 'params': { + 'dmxid': 100001, + 'memberid': 100003 + }, + 'adUnitCode': 'div-gpt-ad-12345678-1', + 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', + 'sizes': [ + [300, 250], + [300, 600] + ], + 'bidId': '29a28a1bbc8a8d', + 'bidderRequestId': '124b579a136515', + 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' +}]; const bidderRequest = { 'bidderCode': 'districtmDMX', 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf', @@ -61,7 +77,7 @@ const bidderRequest = { 'bidder': 'districtmDMX', 'params': { 'dmxid': 100001, - 'memberid': 100003 + 'memberid': 100003, }, 'adUnitCode': 'div-gpt-ad-12345678-1', 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', @@ -420,6 +436,32 @@ const bidderRequest = { 'doneCbCallCount': 0 }; +const bidderRequestNoCoppa = { + 'bidderCode': 'districtmDMX', + 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf', + 'bidderRequestId': '124b579a136515', + 'bids': [{ + 'bidder': 'districtmDMX', + 'params': { + 'dmxid': 100001, + 'memberid': 100003, + }, + 'adUnitCode': 'div-gpt-ad-12345678-1', + 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', + 'sizes': [ + [300, 250], + [300, 600] + ], + 'bidId': '29a28a1bbc8a8d', + 'bidderRequestId': '124b579a136515', + 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' + }], + 'auctionStart': 1529511035677, + 'timeout': 700, + 'start': 1529511035686, + 'doneCbCallCount': 0 +}; + const responses = { 'body': { 'id': '1f45b37c-5298-4934-b517-4d911aadabfd', @@ -522,6 +564,7 @@ describe('DistrictM Adaptor', function () { describe(`buildRequests test usage`, function () { const buildRequestResults = districtm.buildRequests(bidRequest, bidderRequest); + const buildRequestResultsNoCoppa = districtm.buildRequests(bidRequestNoCoppa, bidderRequestNoCoppa); it(`the function should return an array`, function () { expect(buildRequestResults).to.be.an('object'); }); @@ -531,6 +574,16 @@ describe('DistrictM Adaptor', function () { expect(bidr.regs.ext.us_privacy).to.be.equal('1NY'); expect(bidr.user.ext.consent).to.be.an('string'); }); + it(`test contain COPPA`, function() { + const bidr = JSON.parse(buildRequestResults.data) + bidr.regs = bidr.regs || {}; + bidr.regs.coppa = 1; + expect(bidr.regs.coppa).to.be.equal(1) + }) + it(`test should not contain COPPA`, function() { + const bidr = JSON.parse(buildRequestResultsNoCoppa.data) + expect(bidr.regs.coppa).to.be.equal(0) + }) it(`the function should return array length of 1`, function () { expect(buildRequestResults.data).to.be.a('string'); }); From 1175ec21179743ba114f27c7c790eae51b18a842 Mon Sep 17 00:00:00 2001 From: Filip Urbanowicz <61977820+furbanowicz-smartadserver@users.noreply.github.com> Date: Thu, 2 Apr 2020 10:37:15 +0200 Subject: [PATCH 47/90] Added support of supply chain object (#4975) --- modules/smartadserverBidAdapter.js | 51 +++++++++++++------ .../modules/smartadserverBidAdapter_spec.js | 47 ++++++++++++++++- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index 9f85ead5b9b..bff592383a4 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -23,38 +23,54 @@ export const spec = { isBidRequestValid: function (bid) { return !!(bid.params && bid.params.siteId && bid.params.pageId && bid.params.formatId); }, + + /** + * Serialize a supply chain object to a string uri encoded + * + * @param {*} schain object + */ + serializeSupplyChain: function(schain) { + if (!schain || !schain.nodes) return null; + const nodesProperties = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + return `${schain.ver},${schain.complete}!` + + schain.nodes.map(node => nodesProperties.map(prop => + node[prop] ? encodeURIComponent(node[prop]) : '') + .join(',')) + .join('!'); + }, + /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids - * @param {bidderRequest} - bidder request object - * @return ServerRequest Info describing the request to the server. + * @param {BidRequest[]} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest bidder request object + * @return {ServerRequest[]} Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { // use bidderRequest.bids[] to get bidder-dependent request info - // if your bidder supports multiple currencies, use config.getConfig(currency) // to find which one the ad server needs // pull requested transaction ID from bidderRequest.bids[].transactionId return validBidRequests.map(bid => { // Common bid request attributes for banner, outstream and instream. - var payload = { + let payload = { siteid: bid.params.siteId, pageid: bid.params.pageId, formatid: bid.params.formatId, currencyCode: config.getConfig('currency.adServerCurrency'), bidfloor: bid.params.bidfloor || 0.0, - targeting: bid.params.target && bid.params.target != '' ? bid.params.target : undefined, - buid: bid.params.buId && bid.params.buId != '' ? bid.params.buId : undefined, - appname: bid.params.appName && bid.params.appName != '' ? bid.params.appName : undefined, + targeting: bid.params.target && bid.params.target !== '' ? bid.params.target : undefined, + buid: bid.params.buId && bid.params.buId !== '' ? bid.params.buId : undefined, + appname: bid.params.appName && bid.params.appName !== '' ? bid.params.appName : undefined, ckid: bid.params.ckId || 0, tagId: bid.adUnitCode, pageDomain: bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer ? bidderRequest.refererInfo.referer : undefined, transactionId: bid.transactionId, timeout: config.getConfig('bidderTimeout'), bidId: bid.bidId, - prebidVersion: '$prebid.version$' + prebidVersion: '$prebid.version$', + schain: spec.serializeSupplyChain(bid.schain) }; const videoMediaType = utils.deepAccess(bid, 'mediaTypes.video'); @@ -66,7 +82,7 @@ export const spec = { })); } else if (videoMediaType && videoMediaType.context === 'instream') { // Specific attributes for instream. - var playerSize = videoMediaType.playerSize[0]; + let playerSize = videoMediaType.playerSize[0]; payload.isVideo = true; payload.videoData = { videoProtocol: bid.params.video.protocol, @@ -88,6 +104,7 @@ export const spec = { } var payloadString = JSON.stringify(payload); + return { method: 'POST', url: (bid.params.domain !== undefined ? bid.params.domain : 'https://prg.smartadserver.com') + '/prebid/v1', @@ -95,20 +112,22 @@ export const spec = { }; }); }, + /** * Unpack the response from the server into a list of bids. * * @param {*} serverResponse A successful response from the server. + * @param {*} bidRequestString * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function (serverResponse, bidRequestString) { const bidResponses = []; - var response = serverResponse.body; + let response = serverResponse.body; try { if (response) { const bidRequest = JSON.parse(bidRequestString.data); - var bidResponse = { + let bidResponse = { requestId: bidRequest.bidId, cpm: response.cpm, width: response.width, @@ -136,15 +155,16 @@ export const spec = { } return bidResponses; }, + /** * User syncs. * * @param {*} syncOptions Publisher prebid configuration. * @param {*} serverResponses A successful response from the server. - * @return {Syncs[]} An array of syncs that should be executed. + * @return {syncs[]} An array of syncs that should be executed. */ getUserSyncs: function (syncOptions, serverResponses) { - const syncs = [] + const syncs = []; if (syncOptions.iframeEnabled && serverResponses.length > 0) { syncs.push({ type: 'iframe', @@ -153,5 +173,6 @@ export const spec = { } return syncs; } -} +}; + registerBidder(spec); diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index 65b603b9587..5e6397f1b2e 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -42,7 +42,7 @@ describe('Smart bid adapter tests', function () { transactionId: 'zsfgzzg' }]; - // Default params without optional ones + // Default params without optional ones var DEFAULT_PARAMS_WO_OPTIONAL = [{ adUnitCode: 'sas_42', bidId: 'abcd1234', @@ -433,4 +433,49 @@ describe('Smart bid adapter tests', function () { expect(request[1]).to.not.be.empty; }); }); + + describe('Supply Chain Serializer tests', function () { + it('Verify a multi node supply chain serialization matches iab example', function() { + let schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + }, + { + 'asi': 'exchange2.com', + 'sid': 'abcd', + 'hp': 1, + 'rid': 'bid-request-2', + 'name': 'intermediary', + 'domain': 'intermediary.com' + } + ] + }; + + let serializedSchain = spec.serializeSupplyChain(schain); + expect(serializedSchain).to.equal('1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com!exchange2.com,abcd,1,bid-request-2,intermediary,intermediary.com'); + }); + + it('Verifiy that null schain produce null result', function () { + let actual = spec.serializeSupplyChain(null); + expect(null, actual); + }); + + it('Verifiy that schain with null nodes produce null result', function () { + let schain = { + 'ver': '1.0', + 'complete': 1 + + }; + let actual = spec.serializeSupplyChain(null); + expect(null, actual); + }); + }); }); From 9c5156e27bf14f1973e27f064863eef63a3f5cd2 Mon Sep 17 00:00:00 2001 From: Udi Talias Date: Thu, 2 Apr 2020 18:24:54 +0300 Subject: [PATCH 48/90] vidazooBidAdapter: feature/multi size request (#5007) * feat(module): multi size request * fix getUserSyncs added tests * update(module): package-lock.json from master Co-authored-by: roman --- modules/vidazooBidAdapter.js | 96 +++++++++++---------- test/spec/modules/vidazooBidAdapter_spec.js | 76 ++++++++-------- 2 files changed, 94 insertions(+), 78 deletions(-) diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index b3ab8a4d275..d974c0e1eb7 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -1,6 +1,7 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; + export const URL = 'https://prebid.cootlogix.com'; const BIDDER_CODE = 'vidazoo'; const CURRENCY = 'USD'; @@ -19,25 +20,29 @@ function isBidRequestValid(bid) { return !!(params.cId && params.pId); } -function buildRequest(bid, topWindowUrl, size, bidderRequest) { +function buildRequest(bid, topWindowUrl, sizes, bidderRequest) { const {params, bidId} = bid; const {bidFloor, cId, pId, ext} = params; - // Prebid's util function returns AppNexus style sizes (i.e. 300x250) - const [width, height] = size.split('x'); - - const dto = { - method: 'GET', - url: `${URL}/prebid/${cId}`, - data: { - url: encodeURIComponent(topWindowUrl), - cb: Date.now(), - bidFloor: bidFloor, - bidId: bidId, - publisherId: pId, - consent: bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString, - width, - height + let data = { + url: encodeURIComponent(topWindowUrl), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + publisherId: pId, + sizes: sizes, + }; + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + const dto = { + method: 'POST', + url: `${URL}/prebid/multi/${cId}`, + data: data }; utils._each(ext, (value, key) => { @@ -52,10 +57,8 @@ function buildRequests(validBidRequests, bidderRequest) { const requests = []; validBidRequests.forEach(validBidRequest => { const sizes = utils.parseSizesInput(validBidRequest.sizes); - sizes.forEach(size => { - const request = buildRequest(validBidRequest, topWindowUrl, size, bidderRequest); - requests.push(request); - }); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest); + requests.push(request); }); return requests; } @@ -64,23 +67,30 @@ function interpretResponse(serverResponse, request) { if (!serverResponse || !serverResponse.body) { return []; } - const {creativeId, ad, price, exp} = serverResponse.body; - if (!ad || !price) { - return []; - } - const {bidId, width, height} = request.data; + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + try { - return [{ - requestId: bidId, - cpm: price, - width: width, - height: height, - creativeId: creativeId, - currency: CURRENCY, - netRevenue: true, - ttl: exp || TTL_SECONDS, - ad: ad - }]; + results.forEach(result => { + const {creativeId, ad, price, exp, width, height, currency} = result; + if (!ad || !price) { + return; + } + output.push({ + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + ad: ad + }) + }); + return output; } catch (e) { return []; } @@ -101,20 +111,18 @@ function getUserSyncs(syncOptions, responses) { const syncs = []; responses.forEach(response => { const {body} = response; - const cookies = body ? body.cookies || [] : []; - cookies.forEach(cookie => { - switch (cookie.type) { - case INTERNAL_SYNC_TYPE.IFRAME: - break; - case INTERNAL_SYNC_TYPE.IMAGE: + const results = body ? body.results || [] : []; + results.forEach(result => { + (result.cookies || []).forEach(cookie => { + if (cookie.type === INTERNAL_SYNC_TYPE.IMAGE) { if (pixelEnabled && !lookup[cookie.src]) { syncs.push({ type: EXTERNAL_SYNC_TYPE.IMAGE, url: cookie.src }); } - break; - } + } + }); }); }); return syncs; diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 755295c7bd6..a52669b773b 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -22,7 +22,8 @@ const BID = { const BIDDER_REQUEST = { 'gdprConsent': { - 'consentString': 'consent_string' + 'consentString': 'consent_string', + 'gdprApplies': true }, 'refererInfo': { 'referer': 'https://www.greatsite.com' @@ -31,16 +32,20 @@ const BIDDER_REQUEST = { const SERVER_RESPONSE = { body: { - 'ad': '', - 'price': 0.8, - 'creativeId': '12610997325162499419', - 'exp': 30, - 'cookies': [{ - 'src': 'https://sync.com', - 'type': 'iframe' - }, { - 'src': 'https://sync.com', - 'type': 'img' + results: [{ + 'ad': '', + 'price': 0.8, + 'creativeId': '12610997325162499419', + 'exp': 30, + 'width': 300, + 'height': 250, + 'cookies': [{ + 'src': 'https://sync.com', + 'type': 'iframe' + }, { + 'src': 'https://sync.com', + 'type': 'img' + }] }] } }; @@ -119,30 +124,14 @@ describe('VidazooBidAdapter', function () { it('should build request for each size', function () { const requests = adapter.buildRequests([BID], BIDDER_REQUEST); - expect(requests).to.have.length(2); + expect(requests).to.have.length(1); expect(requests[0]).to.deep.equal({ - method: 'GET', - url: `${URL}/prebid/59db6b3b4ffaa70004f45cdc`, + method: 'POST', + url: `${URL}/prebid/multi/59db6b3b4ffaa70004f45cdc`, data: { - consent: 'consent_string', - width: '300', - height: '250', - url: 'https%3A%2F%2Fwww.greatsite.com', - cb: 1000, - bidFloor: 0.1, - bidId: '2d52001cabd527', - publisherId: '59ac17c192832d0011283fe3', - 'ext.param1': 'loremipsum', - 'ext.param2': 'dolorsitamet', - } - }); - expect(requests[1]).to.deep.equal({ - method: 'GET', - url: `${URL}/prebid/59db6b3b4ffaa70004f45cdc`, - data: { - consent: 'consent_string', - width: '300', - height: '600', + gdprConsent: 'consent_string', + gdpr: 1, + sizes: ['300x250', '300x600'], url: 'https%3A%2F%2Fwww.greatsite.com', cb: 1000, bidFloor: 0.1, @@ -158,6 +147,25 @@ describe('VidazooBidAdapter', function () { sandbox.restore(); }); }); + describe('getUserSyncs', function () { + it('should have valid user sync with iframeEnabled', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://static.cootlogix.com/basev/sync/user_sync.html' + }]); + }); + + it('should have valid user sync with pixelEnabled', function () { + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.com', + 'type': 'image' + }]); + }) + }); describe('interpret response', function () { it('should return empty array when there is no response', function () { @@ -193,7 +201,7 @@ describe('VidazooBidAdapter', function () { it('should take default TTL', function () { const serverResponse = utils.deepClone(SERVER_RESPONSE); - delete serverResponse.body.exp; + delete serverResponse.body.results[0].exp; const responses = adapter.interpretResponse(serverResponse, REQUEST); expect(responses).to.have.length(1); expect(responses[0].ttl).to.equal(300); From 95c0d4a31488facf247bbd29db039ff8258d7e8f Mon Sep 17 00:00:00 2001 From: Convergo <61470027+Converge-Digital@users.noreply.github.com> Date: Thu, 2 Apr 2020 18:35:53 +0300 Subject: [PATCH 49/90] Added new Converge Bid Adapter (#5053) * Added new Converge Bid Adapter * Fix JSDoc in Converge Bid Adapter --- modules/convergeBidAdapter.js | 313 +++++++ modules/convergeBidAdapter.md | 57 ++ test/spec/modules/convergeBidAdapter_spec.js | 899 +++++++++++++++++++ 3 files changed, 1269 insertions(+) create mode 100644 modules/convergeBidAdapter.js create mode 100644 modules/convergeBidAdapter.md create mode 100644 test/spec/modules/convergeBidAdapter_spec.js diff --git a/modules/convergeBidAdapter.js b/modules/convergeBidAdapter.js new file mode 100644 index 00000000000..bea3b6cb1ab --- /dev/null +++ b/modules/convergeBidAdapter.js @@ -0,0 +1,313 @@ +import * as utils from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { Renderer } from '../src/Renderer.js'; +import { VIDEO, BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'converge'; +const ENDPOINT_URL = 'https://tech.convergd.com/hb'; +const TIME_TO_LIVE = 360; +const SYNC_URL = 'https://tech.convergd.com/push_sync'; +const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + +let hasSynced = false; + +const LOG_ERROR_MESS = { + noAuid: 'Bid from response has no auid parameter - ', + noAdm: 'Bid from response has no adm parameter - ', + noBid: 'Array of bid objects is empty', + noPlacementCode: "Can't find in requested bids the bid with auid - ", + emptyUids: 'Uids should be not empty', + emptySeatbid: 'Seatbid array from response has empty item', + emptyResponse: 'Response is empty', + hasEmptySeatbidArray: 'Response has empty seatbid array', + hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' +}; +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [ BANNER, VIDEO ], + /** + * 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. + */ + isBidRequestValid: function(bid) { + return !!bid.params.uid; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - an array of bids + * @param {bidderRequest} bidderRequest - bidder request object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const auids = []; + const bidsMap = {}; + const slotsMapByUid = {}; + const sizeMap = {}; + const bids = validBidRequests || []; + let priceType = 'net'; + let pageKeywords; + let reqId; + + bids.forEach(bid => { + if (bid.params.priceType === 'gross') { + priceType = 'gross'; + } + reqId = bid.bidderRequestId; + const {params: {uid}, adUnitCode} = bid; + auids.push(uid); + const sizesId = utils.parseSizesInput(bid.sizes); + + if (!pageKeywords && !utils.isEmpty(bid.params.keywords)) { + const keywords = utils.transformBidderParamKeywords(bid.params.keywords); + + if (keywords.length > 0) { + keywords.forEach(deleteValues); + } + pageKeywords = keywords; + } + + if (!slotsMapByUid[uid]) { + slotsMapByUid[uid] = {}; + } + const slotsMap = slotsMapByUid[uid]; + if (!slotsMap[adUnitCode]) { + slotsMap[adUnitCode] = {adUnitCode, bids: [bid], parents: []}; + } else { + slotsMap[adUnitCode].bids.push(bid); + } + const slot = slotsMap[adUnitCode]; + + sizesId.forEach((sizeId) => { + sizeMap[sizeId] = true; + if (!bidsMap[uid]) { + bidsMap[uid] = {}; + } + + if (!bidsMap[uid][sizeId]) { + bidsMap[uid][sizeId] = [slot]; + } else { + bidsMap[uid][sizeId].push(slot); + } + slot.parents.push({parent: bidsMap[uid], key: sizeId, uid}); + }); + }); + + const payload = { + pt: priceType, + auids: auids.join(','), + sizes: utils.getKeys(sizeMap).join(','), + r: reqId, + wrapperType: 'Prebid_js', + wrapperVersion: '$prebid.version$' + }; + + if (pageKeywords) { + payload.keywords = JSON.stringify(pageKeywords); + } + + if (bidderRequest) { + if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { + payload.u = bidderRequest.refererInfo.referer; + } + if (bidderRequest.timeout) { + payload.wtimeout = bidderRequest.timeout; + } + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + payload.gdpr_applies = + (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') + ? Number(bidderRequest.gdprConsent.gdprApplies) : 1; + } + if (bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } + } + + return { + method: 'GET', + url: ENDPOINT_URL, + data: utils.parseQueryStringParameters(payload).replace(/\&$/, ''), + bidsMap: bidsMap, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @param {*} bidRequest + * @param {Renderer} RendererConst + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest, RendererConst = Renderer) { + serverResponse = serverResponse && serverResponse.body; + const bidResponses = []; + const bidsMap = bidRequest.bidsMap; + const priceType = bidRequest.data.pt; + + let errorMessage; + + if (!serverResponse) errorMessage = LOG_ERROR_MESS.emptyResponse; + else if (serverResponse.seatbid && !serverResponse.seatbid.length) { + errorMessage = LOG_ERROR_MESS.hasEmptySeatbidArray; + } + + if (!errorMessage && serverResponse.seatbid) { + serverResponse.seatbid.forEach(respItem => { + _addBidResponse(_getBidFromResponse(respItem), bidsMap, priceType, bidResponses, RendererConst); + }); + } + if (errorMessage) utils.logError(errorMessage); + return bidResponses; + }, + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + if (!hasSynced && syncOptions.pixelEnabled) { + let params = ''; + + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + params += `&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent) { + params += `&us_privacy=${uspConsent}`; + } + + hasSynced = true; + return { + type: 'image', + url: SYNC_URL + params + }; + } + } +}; + +function isPopulatedArray(arr) { + return !!(utils.isArray(arr) && arr.length > 0); +} + +function deleteValues(keyPairObj) { + if (isPopulatedArray(keyPairObj.value) && keyPairObj.value[0] === '') { + delete keyPairObj.value; + } +} + +function _getBidFromResponse(respItem) { + if (!respItem) { + utils.logError(LOG_ERROR_MESS.emptySeatbid); + } else if (!respItem.bid) { + utils.logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); + } else if (!respItem.bid[0]) { + utils.logError(LOG_ERROR_MESS.noBid); + } + return respItem && respItem.bid && respItem.bid[0]; +} + +function _addBidResponse(serverBid, bidsMap, priceType, bidResponses, RendererConst) { + if (!serverBid) return; + let errorMessage; + if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid); + if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); + else { + const awaitingBids = bidsMap[serverBid.auid]; + if (awaitingBids) { + const sizeId = `${serverBid.w}x${serverBid.h}`; + if (awaitingBids[sizeId]) { + const slot = awaitingBids[sizeId][0]; + + const bid = slot.bids.shift(); + const bidResponse = { + requestId: bid.bidId, // bid.bidderRequestId, + bidderCode: spec.code, + cpm: serverBid.price, + width: serverBid.w, + height: serverBid.h, + creativeId: serverBid.auid, // bid.bidId, + currency: 'EUR', + netRevenue: priceType !== 'gross', + ttl: TIME_TO_LIVE, + dealId: serverBid.dealid + }; + if (serverBid.content_type === 'video' || (!serverBid.content_type && bid.mediaTypes && bid.mediaTypes.video)) { + bidResponse.vastXml = serverBid.adm; + bidResponse.mediaType = VIDEO; + bidResponse.adResponse = { + content: bidResponse.vastXml + }; + if (!bid.renderer && (!bid.mediaTypes || !bid.mediaTypes.video || bid.mediaTypes.video.context === 'outstream')) { + bidResponse.renderer = createRenderer(bidResponse, { + id: bid.bidId, + url: RENDERER_URL + }, RendererConst); + } + } else { + bidResponse.ad = serverBid.adm; + bidResponse.mediaType = BANNER; + } + + bidResponses.push(bidResponse); + + if (!slot.bids.length) { + slot.parents.forEach(({parent, key, uid}) => { + const index = parent[key].indexOf(slot); + if (index > -1) { + parent[key].splice(index, 1); + } + if (!parent[key].length) { + delete parent[key]; + if (!utils.getKeys(parent).length) { + delete bidsMap[uid]; + } + } + }); + } + } + } else { + errorMessage = LOG_ERROR_MESS.noPlacementCode + serverBid.auid; + } + } + if (errorMessage) { + utils.logError(errorMessage); + } +} + +function outstreamRender (bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: bid.adResponse + }); + }); +} + +function createRenderer (bid, rendererParams, RendererConst) { + const rendererInst = RendererConst.install({ + id: rendererParams.id, + url: rendererParams.url, + loaded: false + }); + + try { + rendererInst.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + return rendererInst; +} + +export function resetUserSync() { + hasSynced = false; +} + +export function getSyncUrl() { + return SYNC_URL; +} + +registerBidder(spec); diff --git a/modules/convergeBidAdapter.md b/modules/convergeBidAdapter.md new file mode 100644 index 00000000000..ab916a8b3b6 --- /dev/null +++ b/modules/convergeBidAdapter.md @@ -0,0 +1,57 @@ +# Overview + +Module Name: Converge Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@converge-digital.com + +# Description + +Module that connects to Converge demand source to fetch bids. +Converge Bid Adapter supports Banner and Video (instream and outstream). + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "converge", + params: { + uid: '59', + priceType: 'gross' // by default is 'net' + } + } + ] + },{ + code: 'test-div', + sizes: [[728, 90]], + bids: [ + { + bidder: "converge", + params: { + uid: 1, + priceType: 'gross', + keywords: { + brandsafety: ['disaster'], + topic: ['stress', 'fear'] + } + } + } + ] + },{ + code: 'test-div', + sizes: [[640, 360]], + mediaTypes: { video: {} }, + bids: [ + { + bidder: "converge", + params: { + uid: 60 + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/convergeBidAdapter_spec.js b/test/spec/modules/convergeBidAdapter_spec.js new file mode 100644 index 00000000000..e92ed475497 --- /dev/null +++ b/test/spec/modules/convergeBidAdapter_spec.js @@ -0,0 +1,899 @@ +import { expect } from 'chai'; +import { spec, resetUserSync, getSyncUrl } from 'modules/convergeBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('ConvergeAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'converge', + 'params': { + 'uid': '1' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'uid': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + function parseRequest(url) { + const res = {}; + url.split('&').forEach((it) => { + const couple = it.split('='); + res[couple[0]] = decodeURIComponent(couple[1]); + }); + return res; + } + + const bidderRequest = { + refererInfo: { + referer: 'https://example.com' + } + }; + const referrer = bidderRequest.refererInfo.referer; + + let bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90], [300, 250]], + 'bidId': '3150ccb55da321', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '42dbe3a7168a6a', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('should attach valid params to the tag', function () { + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '59'); + expect(payload).to.have.property('sizes', '300x250,300x600'); + expect(payload).to.have.property('r', '22edbae2733bf6'); + expect(payload).to.have.property('wrapperType', 'Prebid_js'); + expect(payload).to.have.property('wrapperVersion', '$prebid.version$'); + }); + + it('sizes must not be duplicated', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '59,59,60'); + expect(payload).to.have.property('sizes', '300x250,300x600,728x90'); + expect(payload).to.have.property('r', '22edbae2733bf6'); + }); + + it('pt parameter must be "gross" if params.priceType === "gross"', function () { + bidRequests[1].params.priceType = 'gross'; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('pt', 'gross'); + expect(payload).to.have.property('auids', '59,59,60'); + expect(payload).to.have.property('sizes', '300x250,300x600,728x90'); + expect(payload).to.have.property('r', '22edbae2733bf6'); + delete bidRequests[1].params.priceType; + }); + + it('pt parameter must be "net" or "gross"', function () { + bidRequests[1].params.priceType = 'some'; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '59,59,60'); + expect(payload).to.have.property('sizes', '300x250,300x600,728x90'); + expect(payload).to.have.property('r', '22edbae2733bf6'); + delete bidRequests[1].params.priceType; + }); + + it('if gdprConsent is present payload must have gdpr params', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {consentString: 'AAA', gdprApplies: true}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '1'); + }); + + it('if gdprApplies is false gdpr_applies must be 0', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {consentString: 'AAA', gdprApplies: false}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '0'); + }); + + it('if gdprApplies is undefined gdpr_applies must be 1', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {consentString: 'AAA'}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '1'); + }); + + it('if usPrivacy is present payload must have us_privacy param', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('us_privacy', '1YNN'); + }); + + it('should convert keyword params to proper form and attaches to request', function () { + const bidRequestWithKeywords = [].concat(bidRequests); + bidRequestWithKeywords[1] = Object.assign({}, + bidRequests[1], + { + params: { + uid: '59', + keywords: { + single: 'val', + singleArr: ['val'], + singleArrNum: [5], + multiValMixed: ['value1', 2, 'value3'], + singleValNum: 123, + emptyStr: '', + emptyArr: [''], + badValue: {'foo': 'bar'} // should be dropped + } + } + } + ); + + const request = spec.buildRequests(bidRequestWithKeywords, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.keywords).to.be.an('string'); + payload.keywords = JSON.parse(payload.keywords); + + expect(payload.keywords).to.deep.equal([{ + 'key': 'single', + 'value': ['val'] + }, { + 'key': 'singleArr', + 'value': ['val'] + }, { + 'key': 'singleArrNum', + 'value': ['5'] + }, { + 'key': 'multiValMixed', + 'value': ['value1', '2', 'value3'] + }, { + 'key': 'singleValNum', + 'value': ['123'] + }, { + 'key': 'emptyStr' + }, { + 'key': 'emptyArr' + }]); + }); + }); + + describe('interpretResponse', function () { + const responses = [ + {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 59, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 60, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.15, 'adm': '
test content 3
', 'auid': 59, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'price': 0, 'auid': 61, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0, 'adm': '
test content 5
', 'h': 250, 'w': 300}], 'seat': '1'}, + undefined, + {'bid': [], 'seat': '1'}, + {'seat': '1'}, + ]; + + it('should get correct bid response', function () { + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '659423fff799cb', + 'bidderRequestId': '5f2009617a7c0a', + 'auctionId': '1cbd2feafe5e8b', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '659423fff799cb', + 'cpm': 1.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 1
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': [responses[0]]}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('should get correct multi bid response', function () { + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d71a5b', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '4dff80cc4ee346', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '5703af74d0472a', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '300bfeb0d71a5b', + 'cpm': 1.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 1
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '4dff80cc4ee346', + 'cpm': 0.5, + 'creativeId': 60, + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'ad': '
test content 2
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '5703af74d0472a', + 'cpm': 0.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 728, + 'height': 90, + 'ad': '
test content 3
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': responses.slice(0, 3)}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('handles wrong and nobid responses', function () { + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '61' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d7190gf', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '65' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d71321', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '70' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '300bfeb0d7183bb', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', + } + ]; + const request = spec.buildRequests(bidRequests); + const result = spec.interpretResponse({'body': {'seatbid': responses.slice(3)}}, request); + expect(result.length).to.equal(0); + }); + + it('complicated case', function () { + const fullResponse = [ + {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 59, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 60, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.15, 'adm': '
test content 3
', 'auid': 59, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'price': 0.15, 'adm': '
test content 4
', 'auid': 59, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.5, 'adm': '
test content 5
', 'auid': 60, 'h': 600, 'w': 350}], 'seat': '1'}, + ]; + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '2164be6358b9', + 'bidderRequestId': '106efe3247', + 'auctionId': '32a1f276cb87cb8', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '326bde7fbf69', + 'bidderRequestId': '106efe3247', + 'auctionId': '32a1f276cb87cb8', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '4e111f1b66e4', + 'bidderRequestId': '106efe3247', + 'auctionId': '32a1f276cb87cb8', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '26d6f897b516', + 'bidderRequestId': '106efe3247', + 'auctionId': '32a1f276cb87cb8', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '1751cd90161', + 'bidderRequestId': '106efe3247', + 'auctionId': '32a1f276cb87cb8', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '2164be6358b9', + 'cpm': 1.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 1
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '4e111f1b66e4', + 'cpm': 0.5, + 'creativeId': 60, + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'ad': '
test content 2
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '26d6f897b516', + 'cpm': 0.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 728, + 'height': 90, + 'ad': '
test content 3
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '326bde7fbf69', + 'cpm': 0.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'ad': '
test content 4
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': fullResponse}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('dublicate uids and sizes in one slot', function () { + const fullResponse = [ + {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 59, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 59, 'h': 250, 'w': 300}], 'seat': '1'}, + ]; + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '5126e301f4be', + 'bidderRequestId': '171c5405a390', + 'auctionId': '35bcbc0f7e79c', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '57b2ebe70e16', + 'bidderRequestId': '171c5405a390', + 'auctionId': '35bcbc0f7e79c', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '225fcd44b18c', + 'bidderRequestId': '171c5405a390', + 'auctionId': '35bcbc0f7e79c', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '5126e301f4be', + 'cpm': 1.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 1
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '57b2ebe70e16', + 'cpm': 0.5, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 2
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': fullResponse}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + }); + + it('should get correct video bid response', function () { + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '58' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '57dfefb80eca', + 'bidderRequestId': '20394420a762a2', + 'auctionId': '140132d07b031', + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': 'e893c787c22dd', + 'bidderRequestId': '20394420a762a2', + 'auctionId': '140132d07b031', + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + } + ]; + const response = [ + {'bid': [{'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 58, content_type: 'video', w: 300, h: 600}], 'seat': '2'}, + {'bid': [{'price': 1.00, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 60, content_type: 'video'}], 'seat': '2'} + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '57dfefb80eca', + 'cpm': 1.15, + 'creativeId': 58, + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'video', + 'netRevenue': true, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + } + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': response}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('should have right renderer in the bid response', function () { + const spySetRenderer = sinon.spy(); + const stubRenderer = { + setRender: spySetRenderer + }; + const spyRendererInstall = sinon.spy(function() { return stubRenderer; }); + const stubRendererConst = { + install: spyRendererInstall + }; + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '58' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': 'e6e65553fc8', + 'bidderRequestId': '1380f393215dc7', + 'auctionId': '10b8d2f3c697e3', + 'mediaTypes': { + 'video': { + 'context': 'outstream' + } + } + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': 'c8fdcb3f269f', + 'bidderRequestId': '1380f393215dc7', + 'auctionId': '10b8d2f3c697e3' + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '61' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '1de036c37685', + 'bidderRequestId': '1380f393215dc7', + 'auctionId': '10b8d2f3c697e3', + 'renderer': {} + } + ]; + const response = [ + {'bid': [{'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 58, content_type: 'video', w: 300, h: 600}], 'seat': '2'}, + {'bid': [{'price': 1.00, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 60, content_type: 'video', w: 300, h: 250}], 'seat': '2'}, + {'bid': [{'price': 1.20, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 61, content_type: 'video', w: 300, h: 250}], 'seat': '2'} + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': 'e6e65553fc8', + 'cpm': 1.15, + 'creativeId': 58, + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'video', + 'netRevenue': true, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + }, + 'renderer': stubRenderer + }, + { + 'requestId': 'c8fdcb3f269f', + 'cpm': 1.00, + 'creativeId': 60, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'video', + 'netRevenue': true, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + }, + 'renderer': stubRenderer + }, + { + 'requestId': '1de036c37685', + 'cpm': 1.20, + 'creativeId': 61, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'video', + 'netRevenue': true, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + } + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': response}}, request, stubRendererConst); + + expect(spySetRenderer.calledTwice).to.equal(true); + expect(spySetRenderer.getCall(0).args[0]).to.be.a('function'); + expect(spySetRenderer.getCall(1).args[0]).to.be.a('function'); + + expect(spyRendererInstall.calledTwice).to.equal(true); + expect(spyRendererInstall.getCall(0).args[0]).to.deep.equal({ + id: 'e6e65553fc8', + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + loaded: false + }); + expect(spyRendererInstall.getCall(1).args[0]).to.deep.equal({ + id: 'c8fdcb3f269f', + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + loaded: false + }); + + expect(result).to.deep.equal(expectedResponse); + }); + + describe('user sync', function () { + const syncUrl = getSyncUrl(); + + beforeEach(function () { + resetUserSync(); + }); + + it('should register sync image', function () { + let syncs = spec.getUserSyncs({ + pixelEnabled: true + }); + + expect(syncs).to.deep.equal({type: 'image', url: syncUrl}); + }); + + it('should not register sync image more than once', function () { + let syncs = spec.getUserSyncs({ + pixelEnabled: true + }); + expect(syncs).to.deep.equal({type: 'image', url: syncUrl}); + + // when called again, should still have only been called once + syncs = spec.getUserSyncs(); + expect(syncs).to.equal(undefined); + }); + + it('should pass gdpr params if consent is true', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + gdprApplies: true, consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr=1&gdpr_consent=foo` + }); + }); + + it('should pass gdpr params if consent is false', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + gdprApplies: false, consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr=0&gdpr_consent=foo` + }); + }); + + it('should pass gdpr param gdpr_consent only when gdprApplies is undefined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr_consent=foo` + }); + }); + + it('should pass no params if gdpr consentString is not defined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {})).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is a number', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: 0 + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is null', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: null + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is a object', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: {} + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr is not defined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined)).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass usPrivacy param if it is available', function() { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {}, '1YNN')).to.deep.equal({ + type: 'image', url: `${syncUrl}&us_privacy=1YNN` + }); + }); + }); +}); From 10ea4869ce6bb1b1a00d7ffa85f84fe3ee7c43e1 Mon Sep 17 00:00:00 2001 From: e-volution-tech <61746103+e-volution-tech@users.noreply.github.com> Date: Thu, 2 Apr 2020 19:55:40 +0300 Subject: [PATCH 50/90] e-volution-tech: change file names (#5060) * initial * fix * fix * fix * fix * fix * fix * fix * fix * fix * fixes * navigator check * navigator.language check * fix namings * fix namings --- modules/e_volutionBidAdapter.js | 111 +++++++++ modules/e_volutionBidAdapter.md | 53 ++++ .../spec/modules/e_volutionBidAdapter_spec.js | 235 ++++++++++++++++++ 3 files changed, 399 insertions(+) create mode 100644 modules/e_volutionBidAdapter.js create mode 100644 modules/e_volutionBidAdapter.md create mode 100644 test/spec/modules/e_volutionBidAdapter_spec.js diff --git a/modules/e_volutionBidAdapter.js b/modules/e_volutionBidAdapter.js new file mode 100644 index 00000000000..9fc7035db32 --- /dev/null +++ b/modules/e_volutionBidAdapter.js @@ -0,0 +1,111 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const BIDDER_CODE = 'e_volution'; +const AD_URL = 'https://service.e-volution.ai/?c=o&m=multi'; +const URL_SYNC = 'https://service.e-volution.ai/?c=o&m=sync'; +const NO_SYNC = true; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency) { + return false; + } + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl); + case NATIVE: + return Boolean(bid.native && bid.native.title && bid.native.image && bid.native.impressionTrackers); + default: + return false; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + noSync: NO_SYNC, + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.placementId))); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + let winTop = window; + let location; + try { + location = new URL(bidderRequest.refererInfo.referer) + winTop = window.top; + } catch (e) { + location = winTop.location; + utils.logMessage(e); + }; + let placements = []; + let request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + 'secure': 1, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent + } + } + const len = validBidRequests.length; + + for (let i = 0; i < len; i++) { + let bid = validBidRequests[i]; + let traff = bid.params.traffic || BANNER + + placements.push({ + placementId: bid.params.placementId, + bidId: bid.bidId, + sizes: bid.mediaTypes && bid.mediaTypes[traff] && bid.mediaTypes[traff].sizes ? bid.mediaTypes[traff].sizes : [], + traffic: traff + }); + if (bid.schain) { + placements.schain = bid.schain; + } + } + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses) => { + if (NO_SYNC) { + return false + } else { + return [{ + type: 'image', + url: URL_SYNC + }]; + } + } + +}; + +registerBidder(spec); diff --git a/modules/e_volutionBidAdapter.md b/modules/e_volutionBidAdapter.md new file mode 100644 index 00000000000..ff5b2860e05 --- /dev/null +++ b/modules/e_volutionBidAdapter.md @@ -0,0 +1,53 @@ +# Overview + +``` +Module Name: e_volution Bidder Adapter +Module Type: Bidder Adapter +``` + +# Description + +Module that connects to e-volution-tech demand sources + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'placementId_0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'e_volution', + params: { + placementId: 0, + traffic: 'banner' + } + } + ] + }, + // Will return test vast xml. All video params are stored under placement in publishers UI + { + code: 'placementId_0', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'e_volution', + params: { + placementId: 0, + traffic: 'video' + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/e_volutionBidAdapter_spec.js b/test/spec/modules/e_volutionBidAdapter_spec.js new file mode 100644 index 00000000000..447420616d1 --- /dev/null +++ b/test/spec/modules/e_volutionBidAdapter_spec.js @@ -0,0 +1,235 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/e_volutionBidAdapter.js'; + +describe('EvolutionTechBidAdapter', function () { + let bid = { + bidId: '23fhj33i987f', + bidder: 'e_volution', + params: { + placementId: 0, + traffic: 'banner' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and placementId parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://service.e-volution.ai/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes'); + expect(placement.placementId).to.equal(0); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal('banner'); + }); + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + describe('getUserSyncs', function () { + let userSync = spec.getUserSyncs(); + it('Returns valid URL and type', function () { + if (spec.noSync) { + expect(userSync).to.be.equal(false); + } else { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync[0].type).to.be.equal('image'); + expect(userSync[0].url).to.be.equal('https://service.e-volution.ai/?c=o&m=sync'); + } + }); + }); +}); From 2b0c8a5d3ef9f77c2a0b7acbb8bc8ec095527704 Mon Sep 17 00:00:00 2001 From: bretg Date: Thu, 2 Apr 2020 16:38:03 -0400 Subject: [PATCH 51/90] evolution_tech: completing file rename from 5060 (#5068) * evolution_tech: completing file rename from 5060 * removing test cases --- modules/evolution_techBidAdapter.js | 111 --------- modules/evolution_techBidAdapter.md | 53 ---- .../modules/evolution_techBidAdapter_spec.js | 235 ------------------ 3 files changed, 399 deletions(-) delete mode 100644 modules/evolution_techBidAdapter.js delete mode 100644 modules/evolution_techBidAdapter.md delete mode 100644 test/spec/modules/evolution_techBidAdapter_spec.js diff --git a/modules/evolution_techBidAdapter.js b/modules/evolution_techBidAdapter.js deleted file mode 100644 index 9fc7035db32..00000000000 --- a/modules/evolution_techBidAdapter.js +++ /dev/null @@ -1,111 +0,0 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import * as utils from '../src/utils.js'; - -const BIDDER_CODE = 'e_volution'; -const AD_URL = 'https://service.e-volution.ai/?c=o&m=multi'; -const URL_SYNC = 'https://service.e-volution.ai/?c=o&m=sync'; -const NO_SYNC = true; - -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { - return false; - } - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl); - case NATIVE: - return Boolean(bid.native && bid.native.title && bid.native.image && bid.native.impressionTrackers); - default: - return false; - } -} - -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO, NATIVE], - noSync: NO_SYNC, - - isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.placementId))); - }, - - buildRequests: (validBidRequests = [], bidderRequest) => { - let winTop = window; - let location; - try { - location = new URL(bidderRequest.refererInfo.referer) - winTop = window.top; - } catch (e) { - location = winTop.location; - utils.logMessage(e); - }; - let placements = []; - let request = { - 'deviceWidth': winTop.screen.width, - 'deviceHeight': winTop.screen.height, - 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', - 'secure': 1, - 'host': location.host, - 'page': location.pathname, - 'placements': placements - }; - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent - } - } - const len = validBidRequests.length; - - for (let i = 0; i < len; i++) { - let bid = validBidRequests[i]; - let traff = bid.params.traffic || BANNER - - placements.push({ - placementId: bid.params.placementId, - bidId: bid.bidId, - sizes: bid.mediaTypes && bid.mediaTypes[traff] && bid.mediaTypes[traff].sizes ? bid.mediaTypes[traff].sizes : [], - traffic: traff - }); - if (bid.schain) { - placements.schain = bid.schain; - } - } - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses) => { - if (NO_SYNC) { - return false - } else { - return [{ - type: 'image', - url: URL_SYNC - }]; - } - } - -}; - -registerBidder(spec); diff --git a/modules/evolution_techBidAdapter.md b/modules/evolution_techBidAdapter.md deleted file mode 100644 index ff5b2860e05..00000000000 --- a/modules/evolution_techBidAdapter.md +++ /dev/null @@ -1,53 +0,0 @@ -# Overview - -``` -Module Name: e_volution Bidder Adapter -Module Type: Bidder Adapter -``` - -# Description - -Module that connects to e-volution-tech demand sources - -# Test Parameters -``` - var adUnits = [ - // Will return static test banner - { - code: 'placementId_0', - mediaTypes: { - banner: { - sizes: [[300, 250]], - } - }, - bids: [ - { - bidder: 'e_volution', - params: { - placementId: 0, - traffic: 'banner' - } - } - ] - }, - // Will return test vast xml. All video params are stored under placement in publishers UI - { - code: 'placementId_0', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'instream' - } - }, - bids: [ - { - bidder: 'e_volution', - params: { - placementId: 0, - traffic: 'video' - } - } - ] - } - ]; -``` diff --git a/test/spec/modules/evolution_techBidAdapter_spec.js b/test/spec/modules/evolution_techBidAdapter_spec.js deleted file mode 100644 index 54e92db4847..00000000000 --- a/test/spec/modules/evolution_techBidAdapter_spec.js +++ /dev/null @@ -1,235 +0,0 @@ -import {expect} from 'chai'; -import {spec} from '../../../modules/evolution_techBidAdapter.js'; - -describe('EvolutionTechBidAdapter', function () { - let bid = { - bidId: '23fhj33i987f', - bidder: 'e_volution', - params: { - placementId: 0, - traffic: 'banner' - } - }; - - describe('isBidRequestValid', function () { - it('Should return true if there are bidId, params and placementId parameters present', function () { - expect(spec.isBidRequestValid(bid)).to.be.true; - }); - it('Should return false if at least one of parameters is not present', function () { - delete bid.params.placementId; - expect(spec.isBidRequestValid(bid)).to.be.false; - }); - }); - - describe('buildRequests', function () { - let serverRequest = spec.buildRequests([bid]); - it('Creates a ServerRequest object with method, URL and data', function () { - expect(serverRequest).to.exist; - expect(serverRequest.method).to.exist; - expect(serverRequest.url).to.exist; - expect(serverRequest.data).to.exist; - }); - it('Returns POST method', function () { - expect(serverRequest.method).to.equal('POST'); - }); - it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://service.e-volution.ai/?c=o&m=multi'); - }); - it('Returns valid data if array of bids is valid', function () { - let data = serverRequest.data; - expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); - expect(data.deviceWidth).to.be.a('number'); - expect(data.deviceHeight).to.be.a('number'); - expect(data.language).to.be.a('string'); - expect(data.secure).to.be.within(0, 1); - expect(data.host).to.be.a('string'); - expect(data.page).to.be.a('string'); - let placement = data['placements'][0]; - expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes'); - expect(placement.placementId).to.equal(0); - expect(placement.bidId).to.equal('23fhj33i987f'); - expect(placement.traffic).to.equal('banner'); - }); - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([]); - let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); - }); - describe('interpretResponse', function () { - it('Should interpret banner response', function () { - const banner = { - body: [{ - mediaType: 'banner', - width: 300, - height: 250, - cpm: 0.4, - ad: 'Test', - requestId: '23fhj33i987f', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] - }; - let bannerResponses = spec.interpretResponse(banner); - expect(bannerResponses).to.be.an('array').that.is.not.empty; - let dataItem = bannerResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'mediaType'); - expect(dataItem.requestId).to.equal('23fhj33i987f'); - expect(dataItem.cpm).to.equal(0.4); - expect(dataItem.width).to.equal(300); - expect(dataItem.height).to.equal(250); - expect(dataItem.ad).to.equal('Test'); - expect(dataItem.ttl).to.equal(120); - expect(dataItem.creativeId).to.equal('2'); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal('USD'); - }); - it('Should interpret video response', function () { - const video = { - body: [{ - vastUrl: 'test.com', - mediaType: 'video', - cpm: 0.5, - requestId: '23fhj33i987f', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] - }; - let videoResponses = spec.interpretResponse(video); - expect(videoResponses).to.be.an('array').that.is.not.empty; - - let dataItem = videoResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'mediaType'); - expect(dataItem.requestId).to.equal('23fhj33i987f'); - expect(dataItem.cpm).to.equal(0.5); - expect(dataItem.vastUrl).to.equal('test.com'); - expect(dataItem.ttl).to.equal(120); - expect(dataItem.creativeId).to.equal('2'); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal('USD'); - }); - it('Should interpret native response', function () { - const native = { - body: [{ - mediaType: 'native', - native: { - clickUrl: 'test.com', - title: 'Test', - image: 'test.com', - impressionTrackers: ['test.com'], - }, - ttl: 120, - cpm: 0.4, - requestId: '23fhj33i987f', - creativeId: '2', - netRevenue: true, - currency: 'USD', - }] - }; - let nativeResponses = spec.interpretResponse(native); - expect(nativeResponses).to.be.an('array').that.is.not.empty; - - let dataItem = nativeResponses[0]; - expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); - expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') - expect(dataItem.requestId).to.equal('23fhj33i987f'); - expect(dataItem.cpm).to.equal(0.4); - expect(dataItem.native.clickUrl).to.equal('test.com'); - expect(dataItem.native.title).to.equal('Test'); - expect(dataItem.native.image).to.equal('test.com'); - expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; - expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); - expect(dataItem.ttl).to.equal(120); - expect(dataItem.creativeId).to.equal('2'); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal('USD'); - }); - it('Should return an empty array if invalid banner response is passed', function () { - const invBanner = { - body: [{ - width: 300, - cpm: 0.4, - ad: 'Test', - requestId: '23fhj33i987f', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] - }; - - let serverResponses = spec.interpretResponse(invBanner); - expect(serverResponses).to.be.an('array').that.is.empty; - }); - it('Should return an empty array if invalid video response is passed', function () { - const invVideo = { - body: [{ - mediaType: 'video', - cpm: 0.5, - requestId: '23fhj33i987f', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] - }; - let serverResponses = spec.interpretResponse(invVideo); - expect(serverResponses).to.be.an('array').that.is.empty; - }); - it('Should return an empty array if invalid native response is passed', function () { - const invNative = { - body: [{ - mediaType: 'native', - clickUrl: 'test.com', - title: 'Test', - impressionTrackers: ['test.com'], - ttl: 120, - requestId: '23fhj33i987f', - creativeId: '2', - netRevenue: true, - currency: 'USD', - }] - }; - let serverResponses = spec.interpretResponse(invNative); - expect(serverResponses).to.be.an('array').that.is.empty; - }); - it('Should return an empty array if invalid response is passed', function () { - const invalid = { - body: [{ - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] - }; - let serverResponses = spec.interpretResponse(invalid); - expect(serverResponses).to.be.an('array').that.is.empty; - }); - }); - describe('getUserSyncs', function () { - let userSync = spec.getUserSyncs(); - it('Returns valid URL and type', function () { - if (spec.noSync) { - expect(userSync).to.be.equal(false); - } else { - expect(userSync).to.be.an('array').with.lengthOf(1); - expect(userSync[0].type).to.exist; - expect(userSync[0].url).to.exist; - expect(userSync[0].type).to.be.equal('image'); - expect(userSync[0].url).to.be.equal('https://service.e-volution.ai/?c=o&m=sync'); - } - }); - }); -}); From 785aa9fc0d651e491dd29863384297fc22d0d83a Mon Sep 17 00:00:00 2001 From: hybrid-ai <58724131+hybrid-ai@users.noreply.github.com> Date: Fri, 3 Apr 2020 03:34:40 +0300 Subject: [PATCH 52/90] Added Hybrid.ai adapter (#4566) * Added Hybrid.ai adapter * Is used 'find' from 'core-js/library/fn/array/find' instead Array.find * Fixed missing file extensions for imports * Typo fixed * Fixed missing file extensions for imports Co-authored-by: s.shevtsov --- modules/hybridBidAdapter.js | 198 +++++++++++++++ modules/hybridBidAdapter.md | 54 ++++ test/spec/modules/hybridBidAdapter_spec.js | 275 +++++++++++++++++++++ 3 files changed, 527 insertions(+) create mode 100644 modules/hybridBidAdapter.js create mode 100644 modules/hybridBidAdapter.md create mode 100644 test/spec/modules/hybridBidAdapter_spec.js diff --git a/modules/hybridBidAdapter.js b/modules/hybridBidAdapter.js new file mode 100644 index 00000000000..34a4cd9e5d6 --- /dev/null +++ b/modules/hybridBidAdapter.js @@ -0,0 +1,198 @@ +import * as utils from '../src/utils.js' +import { registerBidder } from '../src/adapters/bidderFactory.js' +import { auctionManager } from '../src/auctionManager.js' +import { BANNER, VIDEO } from '../src/mediaTypes.js' +import {Renderer} from '../src/Renderer.js'; +import find from 'core-js/library/fn/array/find'; + +const BIDDER_CODE = 'hybrid'; +const DSP_ENDPOINT = 'https://hbe198.hybrid.ai/prebidhb'; +const TRAFFIC_TYPE_WEB = 1; +const PLACEMENT_TYPE_BANNER = 1; +const PLACEMENT_TYPE_VIDEO = 2; +const TTL = 60; +const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + +const placementTypes = { + 'banner': PLACEMENT_TYPE_BANNER, + 'video': PLACEMENT_TYPE_VIDEO +}; + +function buildBidRequests(validBidRequests) { + return utils._map(validBidRequests, function(validBidRequest) { + const params = validBidRequest.params; + const bidRequest = { + bidId: validBidRequest.bidId, + transactionId: validBidRequest.transactionId, + sizes: validBidRequest.sizes, + placement: placementTypes[params.placement], + placeId: params.placeId + }; + + return bidRequest; + }) +} + +const outstreamRender = bid => { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [bid.width, bid.height], + targetId: bid.adUnitCode, + rendererOptions: { + showBigPlayButton: false, + showProgressBar: 'bar', + showVolume: false, + allowFullscreen: true, + skippable: false, + content: bid.vastXml + } + }); + }); +} + +const createRenderer = (bid) => { + const renderer = Renderer.install({ + targetId: bid.adUnitCode, + url: RENDERER_URL, + loaded: false + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; +} + +function buildBid(bidData) { + const bid = { + requestId: bidData.bidId, + cpm: bidData.price, + width: bidData.width, + height: bidData.height, + creativeId: bidData.bidId, + currency: bidData.currency, + netRevenue: true, + ttl: TTL + }; + + if (bidData.placement === PLACEMENT_TYPE_VIDEO) { + bid.vastXml = bidData.content; + bid.mediaType = VIDEO; + + let adUnit = find(auctionManager.getAdUnits(), function (unit) { + return unit.transactionId === bidData.transactionId; + }); + + if (adUnit) { + bid.width = adUnit.mediaTypes.video.playerSize[0][0]; + bid.height = adUnit.mediaTypes.video.playerSize[0][1]; + + if (adUnit.mediaTypes.video.context === 'outstream') { + bid.renderer = createRenderer(bid); + } + } + } else { + bid.ad = bidData.content; + bid.mediaType = BANNER; + } + + return bid; +} + +function getMediaTypeFromBid(bid) { + return bid.mediaTypes && Object.keys(bid.mediaTypes)[0] +} + +function hasVideoMandatoryParams(mediaTypes) { + const isHasVideoContext = !!mediaTypes.video && (mediaTypes.video.context === 'instream' || mediaTypes.video.context === 'outstream'); + + const isPlayerSize = + !!utils.deepAccess(mediaTypes, 'video.playerSize') && + utils.isArray(utils.deepAccess(mediaTypes, 'video.playerSize')); + + return isHasVideoContext && isPlayerSize; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + placementTypes: placementTypes, + + /** + * 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. + */ + isBidRequestValid(bid) { + return ( + !!bid.params.placeId && + !!bid.params.placement && + ( + (getMediaTypeFromBid(bid) === BANNER && bid.params.placement === 'banner') || + (getMediaTypeFromBid(bid) === VIDEO && bid.params.placement === 'video' && hasVideoMandatoryParams(bid.mediaTypes)) + ) + ); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests(validBidRequests, bidderRequest) { + const payload = { + url: bidderRequest.refererInfo.referer, + cmp: !!bidderRequest.gdprConsent, + trafficType: TRAFFIC_TYPE_WEB, + bidRequests: buildBidRequests(validBidRequests) + }; + + if (payload.cmp) { + const gdprApplies = bidderRequest.gdprConsent.gdprApplies; + if (gdprApplies !== undefined) payload['ga'] = gdprApplies; + payload['cs'] = bidderRequest.gdprConsent.consentString; + } + + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: DSP_ENDPOINT, + data: payloadString, + options: { + contentType: 'application/json' + } + } + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + let bidRequests = JSON.parse(bidRequest.data).bidRequests; + const serverBody = serverResponse.body; + + if (serverBody && serverBody.bids && utils.isArray(serverBody.bids)) { + return utils._map(serverBody.bids, function(bid) { + let rawBid = find(bidRequests, function (item) { + return item.bidId === bid.bidId; + }); + bid.placement = rawBid.placement; + bid.transactionId = rawBid.transactionId; + bid.placeId = rawBid.placeId; + return buildBid(bid); + }); + } else { + return []; + } + } + +} +registerBidder(spec); diff --git a/modules/hybridBidAdapter.md b/modules/hybridBidAdapter.md new file mode 100644 index 00000000000..245f010970a --- /dev/null +++ b/modules/hybridBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + + +**Module Name**: Hybrid.ai Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: prebid@hybrid.ai + +# Description + +You can use this adapter to get a bid from Hybrid.ai + +About us: https://hybrid.ai + +## Sample Banner Ad Unit + +```js +var adUnits = [{ + code: 'banner_ad_unit', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bids: [{ + bidder: "hybrid", + params: { + placement: "banner", // required + placeId: "5af45ad34d506ee7acad0c26" // required + } + }] +}]; +``` + +## Sample Video Ad Unit + +```js +var adUnits = [{ + code: 'video_ad_unit', + mediaTypes: { + video: { + context: 'outstream', // required + playerSize: [640, 480] // required + } + }, + bids: [{ + bidder: 'hybrid', + params: { + placement: "video", // required + placeId: "5af45ad34d506ee7acad0c26" // required + } + }] +}]; +``` + diff --git a/test/spec/modules/hybridBidAdapter_spec.js b/test/spec/modules/hybridBidAdapter_spec.js new file mode 100644 index 00000000000..e5ad878c9b1 --- /dev/null +++ b/test/spec/modules/hybridBidAdapter_spec.js @@ -0,0 +1,275 @@ +import { expect } from 'chai' +import { spec } from 'modules/hybridBidAdapter.js' + +function getSlotConfigs(mediaTypes, params) { + return { + params: params, + sizes: [], + bidId: '2df8c0733f284e', + bidder: 'hybrid', + mediaTypes: mediaTypes, + transactionId: '31a58515-3634-4e90-9c96-f86196db1459' + } +} + +describe('Hybrid.ai Adapter', function() { + const PLACE_ID = '5af45ad34d506ee7acad0c26'; + const bidderRequest = { + refererInfo: { referer: 'referer' } + } + const bannerMandatoryParams = { + placeId: PLACE_ID, + placement: 'banner' + } + const videoMandatoryParams = { + placeId: PLACE_ID, + placement: 'video' + } + const validBidRequests = [ + getSlotConfigs({ banner: {} }, bannerMandatoryParams), + getSlotConfigs({ video: {playerSize: [[640, 480]]} }, videoMandatoryParams) + ] + describe('isBidRequestValid method', function() { + describe('returns true', function() { + describe('when banner slot config has all mandatory params', () => { + describe('and placement has the correct value', function() { + const slotConfig = getSlotConfigs( + { banner: {} }, + { + placeId: PLACE_ID, + placement: 'banner' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) + describe('when video slot has all mandatory params.', function() { + it('should return true, when video mediatype object are correct.', function() { + const slotConfig = getSlotConfigs( + { + video: { + context: 'instream', + playerSize: [[640, 480]] + } + }, + { + placeId: PLACE_ID, + placement: 'video' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) + }) + }) + }) + describe('returns false', function() { + describe('when params are not correct', function() { + function createSlotconfig(params) { + return getSlotConfigs({ banner: {} }, params) + } + it('does not have the placeId.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placement: 'banner' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have the placement.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placeId: PLACE_ID + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have a the correct placement.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placeId: PLACE_ID, + placement: 'something' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + }) + describe('when video mediaType object is not correct.', function() { + function createVideoSlotconfig(mediaType) { + return getSlotConfigs(mediaType, { + placeId: PLACE_ID, + placement: 'video' + }) + } + it('is a void object', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ video: {} }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have playerSize.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ video: { context: 'instream' } }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have context', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ + video: { + playerSize: [[640, 480]] + } + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + }) + }) + }) + it('Url params should be correct ', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + expect(request.method).to.equal('POST') + expect(request.url).to.equal('https://hbe198.hybrid.ai/prebidhb') + }) + + describe('buildRequests method', function() { + it('Common data request should be correct', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(Array.isArray(data.bidRequests)).to.equal(true) + expect(data.url).to.equal('referer') + data.bidRequests.forEach(bid => { + expect(bid.bidId).to.equal('2df8c0733f284e') + expect(bid.placeId).to.equal(PLACE_ID) + expect(bid.transactionId).to.equal('31a58515-3634-4e90-9c96-f86196db1459') + }) + }) + + describe('GDPR params', function() { + describe('when there are not consent management platform', function() { + it('cmp should be false', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(false) + }) + }) + describe('when there are consent management platform', function() { + it('cmps should be true and ga should not sended, when gdprApplies is undefined', function() { + bidderRequest['gdprConsent'] = { + gdprApplies: undefined, + consentString: 'consentString' + } + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(true) + expect(Object.keys(data).indexOf('data')).to.equal(-1) + expect(data.cs).to.equal('consentString') + }) + it('cmps should be true and all gdpr parameters should be sended, when there are gdprApplies', function() { + bidderRequest['gdprConsent'] = { + gdprApplies: true, + consentString: 'consentString' + } + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(true) + expect(data.ga).to.equal(true) + expect(data.cs).to.equal('consentString') + }) + }) + }) + + describe('BidRequests params', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + const bidRequests = data.bidRequests + it('should request a Banner', function() { + const bannerBid = bidRequests[0] + expect(bannerBid.placement).to.equal(spec.placementTypes[bannerMandatoryParams.placement]) + }) + it('should request a Video', function() { + const bannerBid = bidRequests[1] + expect(bannerBid.placement).to.equal(spec.placementTypes[videoMandatoryParams.placement]) + }) + }) + }) + + describe('interpret response method', function() { + it('should return a void array, when the server response are not correct.', function() { + const request = { data: JSON.stringify({}) } + const serverResponse = { + body: {} + } + const bids = spec.interpretResponse(serverResponse, request) + expect(typeof bids).to.equal('object') + expect(bids.length).to.equal(0) + }) + it('should return a void array, when the server response have not got bids.', function() { + const request = { data: JSON.stringify({}) } + const serverResponse = { body: { bids: [] } } + const bids = spec.interpretResponse(serverResponse, request) + expect(typeof bids).to.equal('object') + expect(bids.length).to.equal(0) + }) + describe('when the server response return a bid', function() { + describe('the bid is a banner', function() { + it('should return a banner bid', function() { + const request = spec.buildRequests([validBidRequests[0]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: 'html', + width: 100, + height: 100 + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].mediaType).to.equal(spec.supportedMediaTypes[0]) + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].width).to.equal(100) + expect(bids[0].height).to.equal(100) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].ad).to.equal('string') + }) + }) + describe('the bid is a video', function() { + it('should return a video bid', function() { + const request = spec.buildRequests([validBidRequests[1]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: 'html', + width: 100, + height: 100 + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].mediaType).to.equal(spec.supportedMediaTypes[1]) + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].width).to.equal(100) + expect(bids[0].height).to.equal(100) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].vastXml).to.equal('string') + }) + }) + }) + }) +}) From 0d4527af4d7aafdb055e3d10c21b53fc87fb1fbb Mon Sep 17 00:00:00 2001 From: Xavier Leune Date: Fri, 3 Apr 2020 09:45:56 +0200 Subject: [PATCH 53/90] Freewheel: Better size detection and allow multiple bid requests (#5065) * Freewheel: Better size detection and allow multiple bid requests * Use declared mediaTypes Co-authored-by: Xavier Leune --- modules/freewheel-sspBidAdapter.js | 124 +++++++++--------- .../modules/freewheel-sspBidAdapter_spec.js | 36 ++--- 2 files changed, 83 insertions(+), 77 deletions(-) diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index 0073e3ae491..34724658631 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -1,4 +1,5 @@ import * as utils from '../src/utils.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'freewheel-ssp'; @@ -213,7 +214,7 @@ var getOutstreamScript = function(bid) { export const spec = { code: BIDDER_CODE, - supportedMediaTypes: ['banner', 'video'], + supportedMediaTypes: [BANNER, VIDEO], aliases: ['stickyadstv'], // former name for freewheel-ssp /** * Determines whether or not the given bid request is valid. @@ -234,77 +235,82 @@ export const spec = { buildRequests: function(bidRequests, bidderRequest) { // var currency = config.getConfig(currency); - var currentBidRequest = bidRequests[0]; - if (bidRequests.length > 1) { - utils.logMessage('Prebid.JS - freewheel bid adapter: only one ad unit is required.'); - } - - var zone = currentBidRequest.params.zoneId; - var timeInMillis = new Date().getTime(); - var keyCode = hashcode(zone + '' + timeInMillis); - var requestParams = { - reqType: 'AdsSetup', - protocolVersion: '2.0', - zoneId: zone, - componentId: getComponentId(currentBidRequest.params.format), - timestamp: timeInMillis, - pKey: keyCode - }; + let buildRequest = (currentBidRequest, bidderRequest) => { + var zone = currentBidRequest.params.zoneId; + var timeInMillis = new Date().getTime(); + var keyCode = hashcode(zone + '' + timeInMillis); + var requestParams = { + reqType: 'AdsSetup', + protocolVersion: '2.0', + zoneId: zone, + componentId: getComponentId(currentBidRequest.params.format), + timestamp: timeInMillis, + pKey: keyCode + }; - // Add GDPR flag and consent string - if (bidderRequest && bidderRequest.gdprConsent) { - requestParams._fw_gdpr_consent = bidderRequest.gdprConsent.consentString; + // Add GDPR flag and consent string + if (bidderRequest && bidderRequest.gdprConsent) { + requestParams._fw_gdpr_consent = bidderRequest.gdprConsent.consentString; - if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { - requestParams._fw_gdpr = bidderRequest.gdprConsent.gdprApplies; + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + requestParams._fw_gdpr = bidderRequest.gdprConsent.gdprApplies; + } } - } - if (currentBidRequest.params.gdpr_consented_providers) { - requestParams._fw_gdpr_consented_providers = currentBidRequest.params.gdpr_consented_providers; - } + if (currentBidRequest.params.gdpr_consented_providers) { + requestParams._fw_gdpr_consented_providers = currentBidRequest.params.gdpr_consented_providers; + } - // Add CCPA consent string - if (bidderRequest && bidderRequest.uspConsent) { - requestParams._fw_us_privacy = bidderRequest.uspConsent; - } + // Add CCPA consent string + if (bidderRequest && bidderRequest.uspConsent) { + requestParams._fw_us_privacy = bidderRequest.uspConsent; + } - var vastParams = currentBidRequest.params.vastUrlParams; - if (typeof vastParams === 'object') { - for (var key in vastParams) { - if (vastParams.hasOwnProperty(key)) { - requestParams[key] = vastParams[key]; + var vastParams = currentBidRequest.params.vastUrlParams; + if (typeof vastParams === 'object') { + for (var key in vastParams) { + if (vastParams.hasOwnProperty(key)) { + requestParams[key] = vastParams[key]; + } } } - } - var location = (bidderRequest && bidderRequest.refererInfo) ? bidderRequest.refererInfo.referer : getTopMostWindow().location.href; - if (isValidUrl(location)) { - requestParams.loc = location; - } + var location = (bidderRequest && bidderRequest.refererInfo) ? bidderRequest.refererInfo.referer : getTopMostWindow().location.href; + if (isValidUrl(location)) { + requestParams.loc = location; + } - var playerSize = []; - if (currentBidRequest.mediaTypes.video && currentBidRequest.mediaTypes.video.playerSize) { - // If mediaTypes is video, get size from mediaTypes.video.playerSize per http://prebid.org/blog/pbjs-3 - playerSize = currentBidRequest.mediaTypes.video.playerSize; - } else if (currentBidRequest.mediaTypes.banner.sizes) { - // If mediaTypes is banner, get size from mediaTypes.banner.sizes per http://prebid.org/blog/pbjs-3 - playerSize = getBiggerSizeWithLimit(currentBidRequest.mediaTypes.banner.sizes, currentBidRequest.mediaTypes.banner.minSizeLimit, currentBidRequest.mediaTypes.banner.maxSizeLimit); - } else { - // Backward compatible code, in case size still pass by sizes in bid request - playerSize = getBiggerSize(currentBidRequest.sizes); - } + var playerSize = []; + if (currentBidRequest.mediaTypes.video && currentBidRequest.mediaTypes.video.playerSize) { + // If mediaTypes is video, get size from mediaTypes.video.playerSize per http://prebid.org/blog/pbjs-3 + if (utils.isArray(currentBidRequest.mediaTypes.video.playerSize[0])) { + playerSize = currentBidRequest.mediaTypes.video.playerSize[0]; + } else { + playerSize = currentBidRequest.mediaTypes.video.playerSize; + } + } else if (currentBidRequest.mediaTypes.banner.sizes) { + // If mediaTypes is banner, get size from mediaTypes.banner.sizes per http://prebid.org/blog/pbjs-3 + playerSize = getBiggerSizeWithLimit(currentBidRequest.mediaTypes.banner.sizes, currentBidRequest.mediaTypes.banner.minSizeLimit, currentBidRequest.mediaTypes.banner.maxSizeLimit); + } else { + // Backward compatible code, in case size still pass by sizes in bid request + playerSize = getBiggerSize(currentBidRequest.sizes); + } - if (playerSize[0] > 0 || playerSize[1] > 0) { - requestParams.playerSize = playerSize[0] + 'x' + playerSize[1]; - } + if (playerSize[0] > 0 || playerSize[1] > 0) { + requestParams.playerSize = playerSize[0] + 'x' + playerSize[1]; + } - return { - method: 'GET', - url: FREEWHEEL_ADSSETUP, - data: requestParams, - bidRequest: currentBidRequest + return { + method: 'GET', + url: FREEWHEEL_ADSSETUP, + data: requestParams, + bidRequest: currentBidRequest + }; }; + + return bidRequests.map(function(currentBidRequest) { + return buildRequest(currentBidRequest, bidderRequest); + }); }, /** diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index 5dc67cc1da6..4b80cce8017 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -103,7 +103,7 @@ describe('freewheelSSP BidAdapter Test', () => { it('should add parameters to the tag', () => { const request = spec.buildRequests(bidRequests); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -113,8 +113,8 @@ describe('freewheelSSP BidAdapter Test', () => { it('sends bid request to ENDPOINT via GET', () => { const request = spec.buildRequests(bidRequests); - expect(request.url).to.contain(ENDPOINT); - expect(request.method).to.equal('GET'); + expect(request[0].url).to.contain(ENDPOINT); + expect(request[0].method).to.equal('GET'); }); it('should add usp consent to the request', () => { @@ -122,7 +122,7 @@ describe('freewheelSSP BidAdapter Test', () => { let bidderRequest = {}; bidderRequest.uspConsent = uspConsentString; const request = spec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -141,7 +141,7 @@ describe('freewheelSSP BidAdapter Test', () => { }; const request = spec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -174,7 +174,7 @@ describe('freewheelSSP BidAdapter Test', () => { it('should add parameters to the tag', () => { const request = spec.buildRequests(bidRequests); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -184,8 +184,8 @@ describe('freewheelSSP BidAdapter Test', () => { it('sends bid request to ENDPOINT via GET', () => { const request = spec.buildRequests(bidRequests); - expect(request.url).to.contain(ENDPOINT); - expect(request.method).to.equal('GET'); + expect(request[0].url).to.contain(ENDPOINT); + expect(request[0].method).to.equal('GET'); }); it('should add usp consent to the request', () => { @@ -193,7 +193,7 @@ describe('freewheelSSP BidAdapter Test', () => { let bidderRequest = {}; bidderRequest.uspConsent = uspConsentString; const request = spec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -212,7 +212,7 @@ describe('freewheelSSP BidAdapter Test', () => { }; const request = spec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -327,7 +327,7 @@ describe('freewheelSSP BidAdapter Test', () => { } ]; - let result = spec.interpretResponse(response, request); + let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); @@ -348,15 +348,15 @@ describe('freewheelSSP BidAdapter Test', () => { } ]; - let result = spec.interpretResponse(response, request); + let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); it('handles nobid responses', () => { - var reqest = spec.buildRequests(formattedBidRequests); + var request = spec.buildRequests(formattedBidRequests); let response = ''; - let result = spec.interpretResponse(response, reqest); + let result = spec.interpretResponse(response, request[0]); expect(result.length).to.equal(0); }); }); @@ -460,7 +460,7 @@ describe('freewheelSSP BidAdapter Test', () => { } ]; - let result = spec.interpretResponse(response, request); + let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); @@ -483,15 +483,15 @@ describe('freewheelSSP BidAdapter Test', () => { } ]; - let result = spec.interpretResponse(response, request); + let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); it('handles nobid responses', () => { - var reqest = spec.buildRequests(formattedBidRequests); + var request = spec.buildRequests(formattedBidRequests); let response = ''; - let result = spec.interpretResponse(response, reqest); + let result = spec.interpretResponse(response, request[0]); expect(result.length).to.equal(0); }); }); From 609486bed84cfbb388dbbdaa293a17871c29c9a2 Mon Sep 17 00:00:00 2001 From: Paul Yang Date: Fri, 3 Apr 2020 05:08:39 -0700 Subject: [PATCH 54/90] IdentityLink check ats after delaySync (#5070) --- modules/identityLinkIdSystem.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index a5b93a7f0ee..47d8ad9ac05 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -42,9 +42,10 @@ export const identityLinkSubmodule = { // use protocol relative urls for http or https const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? '&ct=1&cv=' + gdprConsentString : ''}`; let resp; - // if ats library is initialised, use it to retrieve envelope. If not use standard third party endpoint - if (window.ats) { - resp = function(callback) { + resp = function(callback) { + // Check ats during callback so it has a chance to initialise. + // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint + if (window.ats) { window.ats.retrieveEnvelope(function (envelope) { if (envelope) { callback(JSON.parse(envelope).envelope); @@ -52,15 +53,14 @@ export const identityLinkSubmodule = { getEnvelope(url, callback); } }); - } - } else { - resp = function (callback) { + } else { getEnvelope(url, callback); } - } + }; + return {callback: resp}; } -} +}; // return envelope from third party endpoint function getEnvelope(url, callback) { ajax(url, response => { From 136baaa8939d554b7fec6331a57556e2adc438b4 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen <14229985+Fawke@users.noreply.github.com> Date: Fri, 3 Apr 2020 19:36:46 +0530 Subject: [PATCH 55/90] User ID auctionDelay timeout bugfix (#4982) * auctiontimeout bugfix * move clearTimeout outside forEach * move setTimout above the function call * increase auctionDelay --- modules/userId/index.js | 8 ++++++-- test/spec/modules/userId_spec.js | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/userId/index.js b/modules/userId/index.js index cad513642a5..4a5c570331f 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -144,6 +144,9 @@ let configRegistry = []; /** @type {Submodule[]} */ let submoduleRegistry = []; +/** @type {(number|undefined)} */ +let timeoutID; + /** @type {(number|undefined)} */ export let syncDelay; @@ -256,6 +259,7 @@ function processSubmoduleCallbacks(submodules, cb) { // clear callback, this prop is used to test if all submodule callbacks are complete below submodule.callback = undefined; }); + clearTimeout(timeoutID); } /** @@ -322,9 +326,9 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { } } utils.logInfo(`${MODULE_NAME} - auction delayed by ${auctionDelay} at most to fetch ids`); - processSubmoduleCallbacks(submodulesWithCallbacks, continueCallback); - setTimeout(continueCallback, auctionDelay); + timeoutID = setTimeout(continueCallback, auctionDelay); + processSubmoduleCallbacks(submodulesWithCallbacks, continueCallback); } else { // wait for auction complete before processing submodule callbacks events.on(CONSTANTS.EVENTS.AUCTION_END, function auctionEndHandler() { diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 6ff2095665d..41c4841926a 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -419,7 +419,7 @@ describe('User ID', function() { beforeEach(function() { sandbox = sinon.createSandbox(); - sandbox.stub(global, 'setTimeout'); + sandbox.stub(global, 'setTimeout').returns(2); sandbox.stub(events, 'on'); // remove cookie @@ -597,8 +597,8 @@ describe('User ID', function() { coreStorage.setCookie('MOCKID', JSON.stringify({'MOCKID': '123456778'}), new Date(Date.now() + 5000).toUTCString()); config.setConfig({ - userSync: { - auctionDelay: 33, + usersync: { + auctionDelay: 200, syncDelay: 77, userIds: [{ name: 'mockId', storage: { name: 'MOCKID', type: 'cookie' } From b0a281870bcfbe06514b9867ca89f84b352604c3 Mon Sep 17 00:00:00 2001 From: Oleg Naydenov Date: Fri, 3 Apr 2020 19:40:49 +0200 Subject: [PATCH 56/90] Fidelity: Add SCHAIN Support (#4945) * Add SCHAIN Support * Add SCHAIN support * Add SCHAIN Support - 2 --- modules/fidelityBidAdapter.js | 22 ++++++++++++++++++++ test/spec/modules/fidelityBidAdapter_spec.js | 21 ++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/modules/fidelityBidAdapter.js b/modules/fidelityBidAdapter.js index 7bca0745cc5..f16a8ea96be 100644 --- a/modules/fidelityBidAdapter.js +++ b/modules/fidelityBidAdapter.js @@ -27,6 +27,7 @@ export const spec = { tmax: bidderRequest.timeout, defloc: bidderRequest.refererInfo.referer, referrer: getTopWindowReferrer(), + schain: getSupplyChain(bidRequest.schain), }; setConsentParams(bidderRequest.gdprConsent, bidderRequest.uspConsent, payload); @@ -118,4 +119,25 @@ function setConsentParams(gdprConsent, uspConsent, payload) { } } +function getSupplyChain(schain) { + var supplyChain = ''; + if (schain != null && schain.nodes) { + supplyChain = schain.ver + ',' + schain.complete; + for (let i = 0; i < schain.nodes.length; i++) { + supplyChain += '!'; + supplyChain += (schain.nodes[i].asi) ? encodeURIComponent(schain.nodes[i].asi) : ''; + supplyChain += ','; + supplyChain += (schain.nodes[i].sid) ? encodeURIComponent(schain.nodes[i].sid) : ''; + supplyChain += ','; + supplyChain += (schain.nodes[i].hp) ? encodeURIComponent(schain.nodes[i].hp) : ''; + supplyChain += ','; + supplyChain += (schain.nodes[i].rid) ? encodeURIComponent(schain.nodes[i].rid) : ''; + supplyChain += ','; + supplyChain += (schain.nodes[i].name) ? encodeURIComponent(schain.nodes[i].name) : ''; + supplyChain += ','; + supplyChain += (schain.nodes[i].domain) ? encodeURIComponent(schain.nodes[i].domain) : ''; + } + } + return supplyChain; +} registerBidder(spec); diff --git a/test/spec/modules/fidelityBidAdapter_spec.js b/test/spec/modules/fidelityBidAdapter_spec.js index 15bb5a1b59e..1232c20b0d7 100644 --- a/test/spec/modules/fidelityBidAdapter_spec.js +++ b/test/spec/modules/fidelityBidAdapter_spec.js @@ -67,7 +67,19 @@ describe('FidelityAdapter', function () { bidId: '2ffb201a808da7', bidderRequestId: '178e34bad3658f', requestId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', - transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'exchange1.com', + sid: '1234', + hp: 1, + rid: 'bid-request-1', + name: 'publisher', + domain: 'publisher.com' + }] + } } ], start: 1472239426002, @@ -78,7 +90,8 @@ describe('FidelityAdapter', function () { } }; - it('should add source and verison to the tag', function () { + it('should add params to the request', function () { + let schainString = '1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com'; const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); const payload = request.data; expect(payload.from).to.exist; @@ -92,9 +105,11 @@ describe('FidelityAdapter', function () { expect(payload.flashver).to.exist; expect(payload.tmax).to.exist; expect(payload.defloc).to.exist; + expect(payload.schain).to.exist.and.to.be.a('string'); + expect(payload.schain).to.equal(schainString); }); - it('should add gdpr consent information to the request', function () { + it('should add consent information to the request', function () { let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; let uspConsentString = '1YN-'; bidderRequest.gdprConsent = { From 345e6180bc9cdffbe7d791546ae696240972d79b Mon Sep 17 00:00:00 2001 From: Salomon Rada Date: Mon, 6 Apr 2020 17:38:52 +0300 Subject: [PATCH 57/90] Gamoshi: Update adaptor features (#5076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add logic to prefer prebid modules over external modules in build process (#4124) * add check in getModules helper function * update logic based on feedback * update node version of project * Improve Digital adapter: adding bid floor, referrer, more native fields (#4103) * Bid floor, https, native ad update * Update the ad server protocol module * Adding referrer * YIELDONE adapter - change urls to adapt https (#4139) * update: change urls to adapt https * fix test code * Added SupplyChain Object support and an onTimeout Callback (#4137) * - Implemented the 'onTimeout' callback to fire a pixel when there's a timeout. - Added the ability to serialize an schain object according to the description provided here: https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/supplychainobject.md * some mods to the schain tag generation * - added tests for schain param checking. * - fixed a malformed url for timeouts * - Removed a trailing ',' while generating a schain param. * Revert "Added SupplyChain Object support and an onTimeout Callback (#4137)" This reverts commit e61b246b45bd2c2390350eaeca693f208b1a3a24. This commit doesn't use the schain module added in #4084 * Nobid Prebid Adapter commit (#4050) * Nobid Prebid Adapter commit * Fixed global replace and unit tests * Fixed find function * Added nobidBidAdapter.md * Removed description and added "Bid Params" section. * Added test siteId 2 for testing. * Refactored the Adapter to remove most references to the nobid object. We still need the nobid object because we have a passback tag in DFP that makes reference to it. * Fix concurrent responses on the page * Cosmetic change to log an error in case of missing ad markup * Keep nobid.bidResponses cross adapters. * Added GDPR support in user sync and added test coverage. gulp test-coverage gulp view-coverage * Padding issues * Fix padding issues * Fix padding * update outstream prod url (#4104) * support pubcid and uids (#4143) * Fix misspelling and minor cleanup of schain docs (#4150) * Prebid 2.31.0 Release * Increment pre version * Rubicon: tuning logged messages (#4157) * Rubicon: tuning logged messages * Update rubiconBidAdapter.js * fixed indentation * Rubicon Video COPPA fix (#4155) * Rubicon Video COPPA fix * Unit test for Rubicon Video COPPA fix * Playground XYZ adapter - iframe usersync bug fix (#4141) * corrected user sync type * removed support for iframe usersync * added unit tests for getUserSyncs * update nvmrc file (#4162) * update gulp-footer package (#4160) * Datablocks bid/analytics adapter (#4128) * add datablocks Analytics and Bidder Adapters * remove preload param * remove preloadid * better coverage of tests * better coverage * IE doesn't support array.find * lint test * update example host * native asset id should be integer * update logic of ad_types field in appnexusBidAdapter (#4065) * Shorten SomoAudience to just Somo (#4163) * Shorten SomoAudience to just Somo * Make package-lock return * Quantcast: Fix for empty video parameters (#4145) * Copy params from bid.params.video. * Added test for missing video parameters. * Include mimes from adunit. * One Video adding Rewarded Video Feature (#4142) * outstream changes * removing global filtet * reverting page * message * adapter change * remove space * testcases * testpage * spaces for test page * renderer exist case * reverting package-lock.json * adding schain object * adding tagid * syntaxx error fix * video.html * space trailing * space * tagid * inventoryId and placement * rewarded video * added unit test case * Module to pass User Ids to DFP (#4140) * first commit * renamed * minor doc change * documentation * small change * EB * removed unused imports * minor changes * reanmaed a const * adding more methods to test shareUserIds module * unit tets cases for shareUserIds * indentation * renamed DFP to GAM * renamed shareUserIds to userIdTargeting * Update userIdTargeting.md * trying to restart CI * digitrust userId case handled * minor comment change * using auctionEnd event instead of requestBids.before * using events.on * Buzzoola bid adapter (#4127) * initial commit for buzzoola adapter * leave only banners for now * fix bid validation * change endpoint url * add video type * restore renderer * fix renderer * add fixed player sizes * switch bids * convert dimentions to strings * write tests * 100% tests * remove new DOM element creation in tests * handle empty response from server * change description * E2e tests for Native and Outstream video Ad formats. (#4116) * reorganize e2e/ tests into separate directories * new test page for e2e-banner testing * add test to check if Banner Ad is getting loaded * change location of the spec files to reflect change in test/e2e directory structure * add test case to check for generation of valid targeting keys * create Native Ad test page * add test case to check validity of the targeting keys and correct rendering of the Ad * update old browser versions to new * update browser version * update title * remove console.log statements * add basic functional test for e2e outstream video ad format * Update LockerDome adUnitId bid param (#4176) This is not a breaking change * fix several issues in appnexus video bids (#4154) * S2s testing disable client side (#4123) * Add microadBidAdapter * Remove unnecessary encodeURIComponent from microadBidAdapter * Submit Advangelists Prebid Adapter * Submit Advangelists Prebid Adapter 1.1 * Correct procudtion endpoint for prebid * analytics update with wrapper name * reverted error merge * New testServerOnly flag * Tests and a bug fix * Removed dead code * Fixes requested in review * Check each adUnit * isTestingServerOnly changes per Eric * Fixed IE 11 bug * More tests * improved test case names * New option to Include deal KVPs when enableSendAllBids === false (#4136) * new option to include KVPs which have deals when enableSendAllBids === false * updating tests to be more realistic * Prebid 2.32.0 Release * increment pre version * Rubicon doc: changing video test zone (#4187) * added schain support to sonobi adapter (#4173) * if schain config is not defined then error should not be thrown (#4165) * if schain config is not defiend then error should not be thrown * relaxed mode nodes param not defined error handled * added test cases for config validation * a curly bracket was missing in the example * Rubicon: updating test params (#4190) * myTargetBidAdapter: support currency config (#4188) * Update README.md (#4193) * Update README.md * Update README.md * cedato bid adapter instream video support (#4153) * Added adxpremium prebid analytics adapter (#4181) * feat(OAFLO-186): added support for schain (#4194) * Sonobi - send entire userid payload (#4196) * added userid param to pass the entire userId payload to sonobis bid request endpoint * removed console log git p * fixed lint * OpenX Adapter fix: updating outdated video examples (#4198) * userId - Add support for refreshing the cached user id (#4082) * [userId] Added support for refreshing the cached user id: refreshInSeconds storage parameter, related tests and implementation in id5 module * [userId] Added support for refreshing the cached user id: refreshInSeconds storage parameter, related tests and implementation in id5 module * UserId - ID5 - Updated doc with new contact point for partners * UserId - Merged getStoredValue and getStoredDate * [UserId] - ID5 - Moved back ID5 in ./modules * UserId - ID5 - Fixed incorrect GDPR condition * [UserId] - Doc update and test cleanup * Prebid 2.33.0 Release * Increment pre version * SupplyChainObject support and fires a pixel onTimeout (#4152) * - Implemented the 'onTimeout' callback to fire a pixel when there's a timeout. - Added the ability to serialize an schain object according to the description provided here: https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/supplychainobject.md * some mods to the schain tag generation * - added tests for schain param checking. * - fixed a malformed url for timeouts * - Removed a trailing ',' while generating a schain param. * - Using the schain object from validBidRequest if present. Reverting to checking if params has it if not. * - reverting changes to merge with master * - Resolving merge issues * Feature/add profile parameter (#4185) * Add optional profile parameter * EMXDigital Bid Adapter: Add video dimensions in request (#4174) * addressed feedback from #3731 ticket * removed commented code from emx test spec * logging removed from spec * flip h & w values from playerSize for video requests * adding Outstream mediaType to EMX Digital * adding device info. update to grab video param. styling changes. * add video dimensions from playerSize * fix test for video dimensions * Added keywords parameter support in TrustX Bid Adapter (#4183) * Add trustx adapter and tests for it * update integration example * Update trustx adapter * Post-review fixes of Trustx adapter * Code improvement for trustx adapter: changed default price type from gross to net * Update TrustX adapter to support the 1.0 version * Make requested changes for TrustX adapter * Updated markdown file for TrustX adapter * Fix TrustX adapter and spec file * Update TrustX adapter: r parameter was added to ad request as cache buster * Add support of gdpr to Trustx Bid Adapter * Add wtimeout to ad request params for TrustX Bid Adapter * TrustX Bid Adapter: remove last ampersand in the ad request * Update TrustX Bid Adapter to support identical uids in parameters * Update TrustX Bid Adapter to ignore bids that sizes do not match the size of the request * Update TrustX Bid Adapter to support instream and outstream video * Added wrapperType and wrapperVersion parameters in ad request for TrustX Bid Adapter * Update TrustX Bid Adapter to use refererInfo instead depricated function utils.getTopWindowUrl * HOTFIX for referrer encodind in TrustX Bid Adapter * Fix test for TrustX Bid Adapter * TrustX Bid Adapter: added keywords passing support * rubicon: avoid passing unknown position (#4207) * rubicon: not passing pos if not specified * added comment * not sending pos for video when undefined * cleaning up test * fixed unit test * correctly reference bidrequest and determine mediatype of bidresponse (#4204) * GumGum: only send gdprConsent when found (#4205) * adds digitrust module, mods gdpr from bool to int * update unit test * only send gdprconsent if present * LKQD: Use refererInfo.referer as fallback pageurl (#4210) * Refactored URL query parameter passthrough for additional values, changed SSP endpoint to v.lkqd.net, and updated associated unit tests * Use refererInfo.referer as fallback pageurl * Removed logs and testing values * [UserId] - ID5 - Fixed case when consentData is undefined (No CMP) (#4215) * create stubs for localStorage in widespaceBidAdapter test file (#4208) * added adId property to adRenderFailed event (#4097) When no bid (therefore no adUnitCode) is available in the adRenderFailed event it can be difficult to identify the erroring slot.But in almost all cases the given slot still has the adId targeting. * OpenX Adapter: Forcing https requests and adding UserID module support for LiveRamp and TTD (#4182) * OpenX Adapter: Updated requests to force https * OpenX Adapter: Added support for TTD's UnifiedID and LiveRamp's IDL * PubMatic to support userId sub-modules (#4191) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * TripleLift support for UnifiedId and IdentityLink (#4197) * Add IdentityLink support and fix UnifiedId. It appears we've been looking for UnifiedId userIds on the bidderRequest object, when they are found on bidRequests. This commit fixes that error, and adds support for IdentityLink. * change maintainer email to group * Added lemma adapter (#4126) * lemmaBidAdapter.js Added lemma bid adapter file * lemmaBidAdapter.md Added lemma bid adapter md file * lemmaBidAdapter_spec.js Added lemma bid adapter test spec file * Update lemmaBidAdapter.js Fixed automated code review alert comparison between inconvertible types * Update lemmaBidAdapter.js Fixed review changes * Update lemmaBidAdapter.md Correct parameter value. * Adkernel adapter new alias (#4221) * Force https scheme for Criteo Bidder (#4227) * assign adapter version number * Ensure that Criteo's bidder is always called through https * Add Video Support for Datablocks Bid Adapter (#4195) * add datablocks Analytics and Bidder Adapters * remove preload param * remove preloadid * better coverage of tests * better coverage * IE doesn't support array.find * lint test * update example host * native asset id should be integer * add datablocks Video * remove isInteger * skip if empty * update adUnit, bidRequest and bidResponse object (#4180) * update adUnit, bidRequest and bidResponse object * add test for mediaTypes object * 3 display banner and video vast support for rads (#4209) * add stv adapter * remove comments from adapter file * start rads adapter * fix adapter and tests * fixes * fix adapter and doc * fix adapter * fix tests * little fix * add ip param * fix dev url * #3 radsBidAdapter.md * #3 radsBidAdapter.md: cleanup * fix code and doc * UserId - Add SameSite and server-side pubcid support (#3869) * Add SameSite and server-side pubcid support * Fix emoteevBidAdapter unit test * added schain to appnexus bid adapter (#4229) * added schain to appnexus bid adapter * semicolon * update doubleclick url (#4179) * Prebid 2.34.0 release * increment pre version * Rubi Analytics handles > 1 bidResponse per bidRequest (#4224) * videoNow bid adapter (#4088) * -- first commit * -- cors and bidder's name fixed * -- almost ready * -- added docs * -- added nurl tracking * -- bid params * -- tests added * -- test fixed * -- replace placeholder in the onBidWon pixel's url * -- commit for restart tests * -- change response data format for display ad * -- tests updated * -- 100% tests coverage * -- a few clean the test's code * -- custom urls from localStorage * -- tests updated * -- a few clean the test's code * -- new init model * -- spec for new init model * -- fix for new init model * -- code cleaned * -- 100% tests coverage * -- 100% tests coverage * -- fixed test * -- commit for restart tests * djax new bidder adapter (#4192) * djax bidder adapter * djax bidder adapter * Update hello_world.html * Added Turk Telekom Bid Adapter (#4203) * Added Turk Telekom Bid Adapter * Fix md file for Turk Telekom Bid Adapter * MicroAd: Use HTTPS in all requests (#4220) * Always use HTTPS endpoint in MicroAd * Update code * Fixed a broken test in MicroAd * Schain: avoiding Object.values as it is breaking on IE11 (#4238) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * avoiding use of Object.values * 3952 delay auction for ids (#4115) * 3952 delay auction for user ids * 3952 add integration example * 3952 add tests * 3952 fix html example * add todos * 3952 continue auction if ids received * 3952 add tests for auction delay * increase test coverage * set config for test * remove todo * add a few more checks to tests * add comment, force tests to rerun * Feature: adUnitBidLimit (#3906) * added new feature to config to limit bids when sendallbids is enabled * cleaned up code. removed extra spaces etc * removed trailing spaces in config * remove .flat() and replaced with spread operator * removed flat function and instead pushing using spread operator * updated to use sendBidsControl instead * updated targeting_spec to test bidLimit * removed trailing spaces from targeting_spec * Update Rubicon Adapter netRevenue default (#4242) * Add microadBidAdapter * Remove unnecessary encodeURIComponent from microadBidAdapter * Submit Advangelists Prebid Adapter * Submit Advangelists Prebid Adapter 1.1 * Correct procudtion endpoint for prebid * analytics update with wrapper name * reverted error merge * update changed default value of netRevenue to true * Removed AdastaMadia from alias (#4255) * Update appnexusBidAdapter.js (#4251) * IdentityLink - change expiration time to 30 days (#4239) * Add coppa support for AppNexus adapter (#4253) * Add coppa support for AppNexus adapter * test name * add new longform e2e tests (#4206) * Konduit module (#4184) * Adding Konduit module * Removed superfluous arguments passed to obtainVastUrl function * Removed superfluous arguments passed to obtainVastUrl function. * Build trigger (empty commit) * Module documentation updated according to the comments * Logic in obtainVastUrl function updated according to the review comment. * Removed hook, enabled eslint * Circle CI runs e2e tests on every push (#4200) * run functional tests on circle ci on push to any remote branch * remove extraneous key from config file * add test.localhost as alias to 127.0.0.1 * check 0: execute circle-ci * move /etc/config to a separate command * change bid partner to rubicon * test appnexus bid adapter in ci * comment browserstack command * remove console.log statement * test1: circle-ci * change reference dev -> prod while loading prebid * add console.log statement * check-2: circle-ci * comment browserstack testing * change bid adapter * change bid adapter * remove test case for checking targeting keys * remove the ci flag * uncomment test for checking correct generation of targeting keys * swap AN -> Rubicon for testing targeting keys * Outcon bid adapter. (#4161) * Outcon bid adapter. * Fix identation * Fixes * Fixes * Fixes * Spec fixes * Fixes * Fix urls * Fix * Fix parameters * Fix space operators * Fix bidder timeout * Update * Fix whitespace * no message * Outcon unit test * no message * no message * no message * no message * Fixes * Fixes * Change url * no message * no message * no message * Added bidId * no message * no message * no message * no message * Wrapping url with html * no message * no message * no message * Adding workflow to run end to end tests (#4230) * Adding workflow to run end to end tests * trying self branch * Update to run at 12 every day * cleanup config using aliases * update branch and cron time * add command * update prebid path for e2e test pages (#4274) * Prebid 2.35.0 release * Increment pre version * Add usersync to adpone adapter (#4245) * add user sync to adpone adapter * move adpone usersync to global variable * added withcredentials to http request * fix http request options * fix http request options * add withCredentials: true * add withCredentials: true * added test coverage to usersync * update sync function * add test coverage * adpone adapter * package lock * add more testing * add more testing * testing for onBidWon fucntion * test onbidwon function * trigger build * Revert GumGum Adapter 2.28 resizing changes (#4277) * changed resizing unit tests to return the first size dimensions in the sizes array * added some changes * reverted adapter changes * SpotX Bid Adapter: Support schain, ID5 object, Google consent object, and hide_skin (#4281) * Add SpotXBidAdapter * Minor updates * Undo testing changes to shared files * Fix relative imports * Remove superfluous imports and write a few more tests * Formatting, ID5 object, Google consent objects - Added ID5 object support - Added Google Consent object - Reformatted indentaiton on spec file * Revert content_width and content_height changes in docs - not sure how these got moved, lets put them back * Remove click_to_replay flag in example - no reason to use this one in the example * Spotx adapter - Add schain support and update unit tests * Update schain path in ORTB 2.3 request body - schain object is now added to ortb request body at request.ext.source.ext.schain * Add hide_skin to documentation - whoops, this got removed, let's add it back * Update Rubicon Analytics Adapter `bidId` to match PBS (#4156) * Add microadBidAdapter * Remove unnecessary encodeURIComponent from microadBidAdapter * Submit Advangelists Prebid Adapter * Submit Advangelists Prebid Adapter 1.1 * Correct procudtion endpoint for prebid * analytics update with wrapper name * reverted error merge * update for rubicon analytics to send seat[].bid.id for PBS video and banner * fixed conditional for server and video or banner * updated with optimized value test for bidid * update changed default value of netRevenue to true * remove var declaration for rightSlot to correct lgtm error for unused variable * update defineSlot div id to match div id defined in html body * update test ad unit test props * revert lock to match remote master * add seatBidId to bidObj in rpBidAdapter interpretResponse * update setTargeting to execute in the bids back handler * remove dev integration test page * meaningless commit to get lgtm to re-run * SmartRTB adapter update (#4246) * modules: Implement SmartRTB adapter and spec. * Fix for-loop syntax to support IE; refactor getDomain out of exported set. * Remove debugs, update doc * Update test for video support * Handle missing syncs. Add video to media types in sample ad unit * Add null response check, update primary endpoint * Note smrtb video requires renderer * Support Vast Track (#4276) * Add microadBidAdapter * Remove unnecessary encodeURIComponent from microadBidAdapter * Submit Advangelists Prebid Adapter * Submit Advangelists Prebid Adapter 1.1 * Correct procudtion endpoint for prebid * analytics update with wrapper name * reverted error merge * update changed default value of netRevenue to true * Add parameters if config.cache.vasttrack is true * Use requestId instead of adId * Test new vasttrack payload params * Removed commented out code * Relaxed conditional check per review * Removed commented out line * Added 1000x250 size (#4295) * prepare vidazoo adapter for v3.0 (#4291) * Improve Digital adapter: support schain (#4286) * LiveIntent Identity Module. (#4178) * LiveIntentIdSystem. Initial implementation. * LiveIntentIdSystem. Removed whitespace. * Fixed typo * Renamed variables, cookiesm added md. * Changed the default identity url. * Composite id, with having more than just the lipbid passed around. * Composite id. * Merge conflict resolution. * Changed docs and param description. * Added typedoc & mentioned liveIntentIdSystem in submodule.json. * Extracted the LiveIntentIdSystem under modules, removed it from default userId modules. * Fixing the 204 + no body scenario. * Added liveIntent to submodule.json * Fixing docs indentation. * Updated prebidServer & specs. * Minor specs update. * updating liveintent eids source (#4300) * updating liveintent eids source these are supposed to be domains * updating unit test * fix appnexusBidAdapter view-script regex (#4289) * fix an view script regex * minor syntax update * 33Across adding bidder specific extension field (#4298) * - add 33across specific ext field for statedAt * - fix unit test for 33Across adapter * PubMatic to support LiveIntent User Id sub-module (#4306) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * supporting LiveIntent Id in PubMatic adapter * updated source for liveintent * Finteza Analytics Adapter: fix cookies (#4292) * fix reading and sending cookies * fix lint errors * clear comments * add unit tests * fix calling of setCookies for IE * clear cookies after test * use own setCookie method inside tests * Update LockerDome adapter to support Prebid 3.0 (#4301) * Returning the `IdResponse` type with an obj + callback. Fix for 4304 (#4305) * Returning the `IdResponse` type with an obj + callback. * Renamed resp -> result. * Removed whitespace. * ShowHeroes adapter - expanded outstream support (#4222) * add ShowHeroes Adapter * ShowHeroes adapter - expanded outstream support * Revert "ShowHeroes adapter - expanded outstream support" This reverts commit bfcdb913b52012b5afbf95a84956b906518a4b51. * ShowHeroes adapter - expanded outstream support * ShowHeroes adapter - fixes (#4222) * ShowHeroes adapter - banner and outstream fixes (#4222) * ShowHeroes adapter - description and outstream changes (#4222) * ShowHeroes adapter - increase test coverage and small fix * [Orbidder-Adapter] Add bidRequestCount and remove bid.params.keyValues (#4264) * initial orbidder version in personal github repo * use adUnits from orbidder_example.html * replace obsolete functions * forgot to commit the test * check if bidderRequest object is available * try to fix weird safari/ie issue * ebayK: add more params * update orbidderBidAdapter.md * use spec. instead of this. for consistency reasons * add bidfloor parameter to params object * fix gdpr object handling * default to consentRequired: false when not explicitly given * wip - use onSetTargeting callback * add tests for onSetTargeting callback * fix params and respective tests * remove not used bid.params.keyValues * add bidRequestCount to orbidder.otto.de/bid Post request * add bidRequestCount to test object defaultBidRequest * PulsePoint: remove usage of deprecated utils method / prep for 3.0 (#4257) * ET-1691: Pulsepoint Analytics adapter for Prebid. (#1) * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: cleanup * ET-1691: minor * ET-1691: revert package.json change * Adding bidRequest to bidFactory.createBid method as per https://github.com/prebid/Prebid.js/issues/509 * ET-1765: Adding support for additional params in PulsePoint adapter (#2) * ET-1850: Fixing https://github.com/prebid/Prebid.js/issues/866 * Minor fix * Adding mandatory parameters to Bid * Removing usage of deprecated utils method * minor refactor * Use isArray method (#4288) * Add Parrable ID submodule (#4266) * add parrable id submodule * fix integration test config * fix var name * always refresh sotredId for parrable * add submodulesThatAlwaysRefresh concept * remove comment * add parrable url as one string * add parrable prod endpoint * use .indexOf instead of .includes * add params to test config * comment failing test * uncomment failing assertion * add parrable ID to prebid server adapter * add parrableIdSystem to .submodules.json * extract parrableId unit tests from userId spec * remove breakline between imports * remove unused param * remove userId generic feature from parrableId module * remove trailing space * fix failing test due to none merged conflict * Prebid 2.36.0 Release * Increment pre version * Support schain module and send bidfloor param in Sharethrough adapter (#4271) * Add support for supply chain object module Story: [#168742394](https://www.pivotaltracker.com/story/show/168742394) Co-authored-by: Josh Becker * Add bidfloor parameter to bid request sent to STX Story: [#168742573](https://www.pivotaltracker.com/story/show/168742573) * Platform One Analytics Adapter (#4233) * Added Y1 Analytics Adapter * rename y1AnalyticsAdapter in yieldoneAnalyticsAdapter * Yieldone Bid Adapter: fixes from lint check * Yieldone Analytics Adapter: fix endpoint protocol * Added spec file for yieldone Analytics Adapter * Fix parrable id integration example (#4317) * fix parrableId integration example * add parentheses * Improve Digital adapter: support for video (#4318) * Bid floor, https, native ad update * Update the ad server protocol module * Adding referrer * Improve Digital support for video * Improve Digital adapter: video * adapter version -> 6.0.0 * Gamoshi: Update aliases list. Add support for userSync. (#4319) * Add support for multi-format ad units. Add favoredMediaType property to params. * Add tests for gdpr consent. * Add adId to outbids * Modify media type resolving * Refactor multi-format ad units handler. * Modify the way of sending GDPR data. Update aliases. * Add new consent fields. Add unit test. * Add new consent fields. Add unit test. * Add support for id5 and unified id cookie sync. * Add support for id5 and unified id cookie sync. * Add restricted check for gdpr consent. * fix for userSync endpoint getting called with bidder alias names, instead of actual bidder names (#4265) * modify ixBidAdapater to always use the secure endpoint (#4323) * PubMatic to support Parrable User Id sub-module (#4324) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * PubMatic to support parrable id * VISX: currency validation & fix double escape of referer (#4299) * PubMatic to support coppa (#4336) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * added coppa compliance * vuble: outstream has fullscreen option (#4320) * Mod: vuble oustream has fullscreen option * Add: vuble test for outstream scenario * EMXDigital: hotfix to resolve URIError from decodeURIComponent (#4333) * hotfix to resolve URIError from decodeURIComponent * added unit for decoding adm * Specify second parameter for parseInt for pubmaticBidAdapter (#4347) * Remove usage of getTopWindowUrl in Prebid Adapter (#4341) * Conversant Bid Adapter update for 3.0 (#4284) * Add cpmDistribution function for Google Analytics adapter (#4240) * Add cpmDistribution function for Google Analytics adapter * Add test for the cpmDistribution function * Remove half written comment * fixing SRA p_pos (#4337) * In Sonobi Adapter, only read sizes from bid.mediaTypes (#4311) * Fix mediaTypes (#4332) * Outcon bid adapter. * Fix identation * Fixes * Fixes * Fixes * Spec fixes * Fixes * Fix urls * Fix * Fix parameters * Fix space operators * Fix bidder timeout * Update * Fix whitespace * no message * Outcon unit test * no message * no message * no message * no message * Fixes * Fixes * Change url * no message * no message * no message * Added bidId * no message * no message * no message * no message * Wrapping url with html * no message * no message * no message * Fix mediaTypes * no message * Update outconBidAdapter_spec.js * Adding VAS response * no message * no message * no message * Fix * Changed ttl * no message * supportedMediaTypes * no message * no message * Prebid 2.37.0 release * increment pre version * Add vast xml support and other minor changes to Beachfront adapter (#4350) * Add support for vast xml in the bid response * add secure protocol to outstream player url * add device connection type * add player setting for poster color * add new value for creative Id * Update smartrtbBidAdapter (#4362) * modules: Implement SmartRTB adapter and spec. * Fix for-loop syntax to support IE; refactor getDomain out of exported set. * Remove debugs, update doc * Update test for video support * Handle missing syncs. Add video to media types in sample ad unit * Add null response check, update primary endpoint * Note smrtb video requires renderer * Remove old params checks, fix documentation playerSize field name * Revert "Update smartrtbBidAdapter (#4362)" (#4368) This reverts commit be6704bcec65a28d80b6d09a8d1c51ef9a8ba824. * Add userSync in onetagBidAdapter (#4358) * Minor bug fixing in onetagBidAdapter.js Fixed a minor bug. Updated TTL in response to align the correct specifications. * Update onetagBidAdapter Added additional page info and user sync function. * Update onetagBidAdapter_spec.js Added the test for getUserSyncs function. * Fix about userSync * getUserSyncs: test update with gdpr params * Sovrn adapter updates: schain, digitrust, pixel syncing, and 3.0 upgrades (#4335) * schain and digitrust * pixel beacons * unit tests and fixes from testing * Prebid 3.0 updates * review fix * Add bid adapter for ablida (#4256) * Add ablida adapter * rename category parameter, add documentation * AdKernel: added waardex_ak alias (#4290) * added alias Added a new alias * fixing unit test * Revert "Sovrn adapter updates: schain, digitrust, pixel syncing, and 3.0 upgrades (#4335)" (#4376) This reverts commit 6114a3dba93815dcfb535707d7b4d84f1adb2bc7. * Vrtcal Markets Inc. Bid Adapter Addition (#4259) * Added 3 key Vrtcal Adapter files: adapter,markdown,unit tests * Removed unused getUserSyncs;Added mediaTypes.banner.sizes support;Raised test coverage to 85% * lint formatting errors corrected * Update schain path in ORTB path for spotxBidAdapter (#4377) - Move schain object from request.ext.source.ext.schain to request.source.ext.schain * Update Grid Bid Adapter (#4379) * Added Grid Bid Adapter * remove priceType from TheMediaGrid Bid Adapter * Add video support in Grid Bid Adapter * Added test parameter for video slot * update Grid Bid Adapter to set size in response bid * Update Grid Bid Adapter to support identical uids in parameters * Fix typo in test file for Grid Bid Adapter * Update The Grid Media Bidder Adapter to send refererInfo.referer as 'u' parameter in ad request * Hotfix for referrer in Grid Bid Adapter * Grid Bid Adapter: added wrapperType and wrappweVersion to the ad request * TripleLift: Sending schain (#4375) * Add IdentityLink support and fix UnifiedId. It appears we've been looking for UnifiedId userIds on the bidderRequest object, when they are found on bidRequests. This commit fixes that error, and adds support for IdentityLink. * change maintainer email to group * TripleLift: Sending schain (#1) * Sending schain * null -> undefined * DistrictmDMX: adding support for schain and remove content type to default to prebid selection (#4366) * adding DMX test @97%, two files added one updated * Update districtm_spec.js * Update districtmDMX.js * adding all districtm needed file * remove legacy file * remove typo || 0 in the test method * force default to return a valid width and height * update unit test code for failing test * changed class for an object * remove package-lock.json * change file name for dmx adapter * renamed files * restaure package-lock.json * update to last package-lock state * update gdpr user consent * fix sizes issue * Documentation updates Adding the readme.md info * update file name and update unit testing import file location * current machine state * lint correction * remove variable assigment duplicate * Support for ID5 + receive meta data (#4352) * Livewrapped bid and analytics adapter * Fixed some tests for browser compatibility * Fixed some tests for browser compatibility * Changed analytics adapter code name * Fix double quote in debug message * modified how gdpr is being passed * Added support for Publisher Common ID Module * Corrections for ttr in analytics * ANalytics updates * Auction start time stamp changed * Detect recovered ad blocked requests Make it possible to pass dynamic parameters to adapter * Collect info on ad units receiving any valid bid * Support for ID5 Pass metadata from adapter * Typo in test + eids on wrong level * Rubicon Adapter: Always make requests using HTTPS (#4380) * Add microadBidAdapter * Remove unnecessary encodeURIComponent from microadBidAdapter * Submit Advangelists Prebid Adapter * Submit Advangelists Prebid Adapter 1.1 * Correct procudtion endpoint for prebid * analytics update with wrapper name * reverted error merge * update changed default value of netRevenue to true * Always make bids requests using https * rp_secure and imp.secure should always be 1 * 7xbid adapter (#4328) * 7xbid adapter * fix error when cli build * - update 33across adapter cookie sync end point (#4345) - update unit test for 33across adapter * Adform adapter: add renderer for outstream bids (#4363) * Prebid 2.38.0 Release * Increment pre version * Adagio: update with external js (#4217) * Add external loader in AdagioBidAdapter * Change adagioAnalyticsAdapter to "endpoint" type * Change _setPredictions for a generic method * Improve AdagioBidAdapter test coverage * Add features detection in Adagio adapter * Fix adagioBidAdapter tests * Add featuresVersion field to bidRequest * Refacto adagio.queue * Expose versions in ADAGIO namespace * Generate a ADAGIO.pageviewId if missing * Move ad-server events tracking to adagioBidAdapter * Store adUnitCodes in ADAGIO namespace * Update documentation Better description of test parameters. * Add internal array to prevent empty pbjs.adUnits * Be sure to access to window.top - does not work in safe-frame env * Add PrintNumber feature * Be sure to compute features on window.top * Bump versions * Add Post-Bid support - ad-server events are listen in current window (instead of window.top) - a new "outerAdUnitElementId" property is set to ADAGIO.pbjsAdUnits array in case of Post-Bid scenario. This property is the 1st parent element id attribute of the iframe in window.top. * Set pagetype param as optional * Add AdThink ad-server support * Improve internal `pbjsAdUnits.sizes` detection Use the adUnit `mediaTypes.banner.sizes` property if exists to build the `ADAGIO.pbjsAdUnits.sizes`. The use of the `sizes` root property is deprecated. * adagioAnalyticsAdapter: add and improve tests * adagioBidAdapter: add and improve tests # Conflicts: # modules/adagioBidAdapter.js # test/spec/modules/adagioBidAdapter_spec.js * adagioBidAdapter: Bump version 1.5 * Adagio: fix import path * PostBid: insure window.top is accessible for specifics functions * Consistency: use Prebid.js utils and fix deprecated * PostBid: do not build a request if in safeframe * Bump version 2.0.0 * Try to fix tests without UA stubing * Try to fix adagioAnalytics failling tests on CI * Consistency: use Prebid loadExternalScript() * Add "adagio" to Prebid.js adloader vendor whitelist * Remove proprietary ad-server listeners * Add RSA validation to adagio external script * add viewdeosDX whitelabel (#4231) * add viewdeosDX hitelabel * Fixed tests and support for sizes * Fix strings * Fix strings * remove only * Fix tests * fix codereview * Fix test + Code review * code review + tests * One video display ad (#4344) * outstream changes * removing global filtet * reverting page * message * adapter change * remove space * testcases * testpage * spaces for test page * renderer exist case * reverting package-lock.json * adding schain object * adding tagid * syntaxx error fix * video.html * space trailing * space * tagid * inventoryId and placement * rewarded video * added unit test case * testing display ad * adding banner * validating banner object * display=1 changes * checking whether diplsy == 1 * html page change * reverting video.html * adding more test cases * spaces * md file change * updated working oneVideoBidAdapter.md file * Update oneVideoBidAdapter.md * Update oneVideoBidAdapter.md * updated the file with both video params and banner * Update video.html * fix double-urlecoded referrer (#4386) * fix double-urlecoded referer (#4388) * PulsePoint Adapter - update for ttl logic (#4400) * ET-1691: Pulsepoint Analytics adapter for Prebid. (#1) * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: cleanup * ET-1691: minor * ET-1691: revert package.json change * Adding bidRequest to bidFactory.createBid method as per https://github.com/prebid/Prebid.js/issues/509 * ET-1765: Adding support for additional params in PulsePoint adapter (#2) * ET-1850: Fixing https://github.com/prebid/Prebid.js/issues/866 * Minor fix * Adding mandatory parameters to Bid * Using the TTL from the bid.ext * Minor refactor * IdentityLink - add logic for sending consent string (#4346) * Fix adagio analytics adapter circleci (#4409) * Add microadBidAdapter * Remove unnecessary encodeURIComponent from microadBidAdapter * Submit Advangelists Prebid Adapter * Submit Advangelists Prebid Adapter 1.1 * Correct procudtion endpoint for prebid * analytics update with wrapper name * reverted error merge * update changed default value of netRevenue to true * update to skip broken circleci tests * skip all * Feature/7xbid remove unneeded params (#4402) * 7xbid adapter * fix error when cli build * remove unneeded params * Empty commit * Empty commit * Remove none ssl (#4406) * adding DMX test @97%, two files added one updated * Update districtm_spec.js * Update districtmDMX.js * adding all districtm needed file * remove legacy file * remove typo || 0 in the test method * force default to return a valid width and height * update unit test code for failing test * changed class for an object * remove package-lock.json * change file name for dmx adapter * renamed files * restaure package-lock.json * update to last package-lock state * update gdpr user consent * fix sizes issue * Documentation updates Adding the readme.md info * update file name and update unit testing import file location * current machine state * lint correction * remove variable assigment duplicate * remove none ssl element from all request] * fixed reference to global object (#4412) * ucfunnel adapter support supply chain (#4383) * Add a new ucfunnel Adapter and test page * Add a new ucfunnel Adapter and test page * 1. Use prebid lib in the repo to keep updated 2. Replace var with let 3. Put JSON.parse(JSON.stringify()) into try catch block * utils.getTopWindowLocation is a function * Change to modules from adapters * Migrate to module design * [Dev Fix] Remove width and height which can be got from ad unit id * Update ucfunnelBidAdapter to fit into new spec * Correct the endpoint. Fix the error of query string * Add test case for ucfunnelBidAdapter * Fix lint error * Update version number * Combine all checks on bid request * Add GDPR support for ucfunnel adapter * Add in-stream video and native support for ucfunnel adapter * Remove demo page. Add more test cases. * Change request method from POST to GET * Remove unnecessary comment * Support vastXml and vastUrl for video request * update TTL to 30 mins * Avoid using arrow function which is not discuraged in mocha * ucfunnel tdid support * ucfunnel adapter support supply chain * LiveIntent support in RP Adapter and PBS Adapter update to pass segments (#4303) * Add microadBidAdapter * Remove unnecessary encodeURIComponent from microadBidAdapter * Submit Advangelists Prebid Adapter * Submit Advangelists Prebid Adapter 1.1 * Correct procudtion endpoint for prebid * analytics update with wrapper name * reverted error merge * update changed default value of netRevenue to true * added semi-colon * update eid source to use domain * update video oRTB with liveintent segments * update pbs adapter with liveintent segments support * update rp adapter liveintent support for fastlane * reverted package lock, fix for unintentional update * added unit tests for fastlane.json and ortb, fix to join segments with commas * fix obj property path data.tpid * update remove unnecessary function call * re-ordering query string params * Rubicon Adapter: Add multiple sizes to sizeMap (#4407) * Add Utils to remove item in LocalStorage (#4355) * Making originalCpm and originalCurrency fields in bid object always available (#4396) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * moving originalCurrency declaration from currency to bidderFactory * added a comment * trying to re-run the CI job * added unit test case * trying to re-run the CI job * Placement and inventory (#4353) * outstream changes * removing global filtet * reverting page * message * adapter change * remove space * testcases * testpage * spaces for test page * renderer exist case * reverting package-lock.json * adding schain object * adding tagid * syntaxx error fix * video.html * space trailing * space * tagid * inventoryId and placement * rewarded video * added unit test case * inventory_id and placement * removed unnecessary file * lint error * Update oneVideoBidAdapter.js * lint error fix * Fixes for Platform One Analytics Adapter (#4359) * Added Y1 Analytics Adapter * rename y1AnalyticsAdapter in yieldoneAnalyticsAdapter * Yieldone Bid Adapter: fixes from lint check * Yieldone Analytics Adapter: fix endpoint protocol * Added spec file for yieldone Analytics Adapter * Add adUnitName to analytics data for Yieldone Analytics Adapter * Fix yieldone Analytics Adapter to log only id from adUnitPath * Fix bug with timeout event in Yieldone Analytics Adapter * Added protocol to url (#4395) * initial commit * updated contact and tag details * changes ti support the renderers * changes to pass dimId * fixed names of internal mapping * added comment * added gdpr param to request and other fixes * modified api url * fix * fixed the secure api call * rolled back video event callback till we support it * updated doc with video details * added bid won and timeout pixel * added testcase for bid events * modified testcase * fixed the url logged * tag param values passed ot renderer * added a conditioal check * changes to support new param to adserver for purpose of tracking * passed param to renderer * missing variable defined * added protocol to url * fixed test for protocol * changed urls to secure only * Update emoteev endpoints (#4329) * JustPremium: Update to Prebid 3.0 (#4410) * Update underdogmedia adapter for pbjs 3.0 (#4390) * Update underdogmedia adapter for pbjs 3.0 * Ensure request to endpoint is secure * Update prebid version * Lint fix * Update Consumable adapter for Prebid.js 3.0 (#4401) * Consumable: Clean up tests. * Consumable: Update use of deprecated function. * Consumable: Read sizes from mediaTypes.banner.sizes. * Consumable: Fix lint violation. * CriteoId User Module (#4287) * Add CriteoId module * Update the return type of getId in Criteo Id module Changes: - Use of url parsing function from url lib - Update the return type of getId() - Update the jsdoc to reflect the real return types * Fix failing tests for Criteo user module * Add CriteoIdSystem submodule to .submodule.json. * 2019/10/18 Create Mobsmart bidder adapter (#4339) * Adpod deal support (#4389) * Adpod deal support * Replacing filterBids with minTier * fix potential issue * remove querystringify package (#4422) * Browsi real time data module (#4114) * real time data module, browsi sub module for real time data, new hook bidsBackCallback, fix for config unsubscribe * change timeout&primary ad server only to auctionDelay update docs * support multiple providers * change promise to callbacks configure submodule on submodules.json * bug fixes * use Prebid ajax * tests fix * Prebid 2.39.0 Release * increment pre version * OpenX Adapter: Prebid 3.0 Compatibility Update (#4413) * Removed usage of deprecated functions * Removed beacons * Banner sizes now reads from bidRequest.mediaTypes.banner.sizes instead of bidRequest.sizes * Updated tests to reflect changes. * GumGum: use mediaTypes.banner.sizes (#4416) * adds digitrust module, mods gdpr from bool to int * update unit test * only send gdprconsent if present * uses mediaTypes before trying bidRequest sizes * removes use of deprecated method * RTBhouse Bid Adapter update for 3.0 (#4428) * add viewable rendering format (#4201) * Feature/adapter (#4219) * feat(bidrequest): code for making bidrequest * feat(bidresponse): format and return the response * feat(tests): added tests for adapter * feat(docs): added docs for the adapter * refactor(url): changed adserver url * test(user sync): added unit tests for the user syncs * refactor(endpoint): changed endpoint for prebid * refactor(endpoint): changed endpoint for prebid * doc(tagid): mandatory param definition added * fix(imp id): fix for correct impression id * fix(width/height): fix for correct width and height sequence * PulsePoint Bid Adapter: Support for schain (#4433) * ET-1691: Pulsepoint Analytics adapter for Prebid. (#1) * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: cleanup * ET-1691: minor * ET-1691: revert package.json change * Adding bidRequest to bidFactory.createBid method as per https://github.com/prebid/Prebid.js/issues/509 * ET-1765: Adding support for additional params in PulsePoint adapter (#2) * ET-1850: Fixing https://github.com/prebid/Prebid.js/issues/866 * Minor fix * Adding mandatory parameters to Bid * ET-5938 SupplyChain Object Support * Formatting * Code review * Code review * Fix to currency parsing on response * Add supply chain support for Teads adapter (#4420) * Rubicon: support SupplyChain (schain) (#4315) * Add microadBidAdapter * Remove unnecessary encodeURIComponent from microadBidAdapter * Submit Advangelists Prebid Adapter * Submit Advangelists Prebid Adapter 1.1 * Correct procudtion endpoint for prebid * analytics update with wrapper name * reverted error merge * update changed default value of netRevenue to true * Starting schain * More tests for banner schain support * Video tests * Encoding tweaks, required fields and comments * Removed .only() from tests * Change requests per Bret * Add 1ad4good bidder (#4081) * adding bidder code and A bidder for non-profit free ads. more info about this bidder project can be found on project site http://1ad4good.org * removed unused code test coverage is improved to >80% tested for instream video support * removed some legacy code, unused params * hardcoding https to endpoint * Improve Digital adapter fix: don't send sizes for instream video (#4427) * Bid floor, https, native ad update * Update the ad server protocol module * Adding referrer * Improve Digital support for video * Improve Digital adapter: video * adapter version -> 6.0.0 * Improve Digital adapter: don't send sizes for video * Fix a typo in code comment (#4450) * Inventory id and schain support for display (#4426) * supporting schain * Update coinzillaBidAdapter.js (#4438) Update sizes const. * Support schain in ZEDO adapter (#4441) * changes to pass schain * PubMatic supporting updated Criteo User Id module (#4431) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * PubMatic supporting updated Criteo User Id module * added a comment to re-start CI * Remove duplicate param to fix unit tests (#4459) * Brightcom Bid Adapter update for 3.0 (#4343) * add support for min_height field in pbs native requests (#4434) * Supporting Alias via Video Requests (#4460) * New adapter Proxistore (#4365) * add test adapter and documentation * integration test with hello_world * reset package-lock.json * delete useless conditionnal * make integrate test work * revert hello-world * revert hello_world * fix descriptor * change adUnits for integration test * remove proxistore widget * uncomment file * change sizes * remove useless script tag * Implementation of setBidderConfig and bidder-specific data (#4334) * initial implementation of setBidderConfig * fix ie11 test errors * Support new setBidderConfig format. Include props from both config and bidderConfig in _getConfig * Use core-js Set to avoid issues with IE * Fix tests in IE * put registerSyncs back on bidderFactory * run bidder event methods with bidder config enabled * Prebid 2.40.0 Release * Increment pre version * Conversant Bid Adapter checks pubcid directly (#4430) * Cookie Sync functionality (#4457) * changing PID param value for testing * cookie sync integration * merge from upstream * Staq Adapter: update with meta envelope (#4372) * initial dev * fix staq adapter name * fix hello world staq call * get hello world working again * add user agent collection * fix some unite tests * Add STAQ Analytics Adapter doc * clean up hello world * fix tests to play nice with browserstack * fix around issues with browserstack and deep equals of objects * dump variable env testing since we can't mod user agent stuff in browserstack * Update STAQ adapter to stop using deprecated utils for referrer * remove package-lock.json changes via master rebase * improve call frequency for ref util * change ajax content type * adjust ajax request to not expect whitelisting * remove superflous commented-out code * update event package to use meta information in envelope rather than per event basis * fix formatting * more formatting fixes * more formatting! * Rhythmone Adapter - schain support (#4414) Circle CI failing tests are not related to this PR. * Media.net Adapter: Support Prebid 3.0 (#4378) * Media.net Adapter: Support Prebid 3.0 * Media.net Adapter: add tests to increase code coverage * Vi Adapter: Passes additional param in the bid request (#4134) * Add focus check (cherry picked from commit 9d6d6dfb83580d6a5ffed8faa5762db48f8fd44d) * Pass focus as numeric value (cherry picked from commit 9fae56a637f87b0d39cc1d24eeb1f9ff9df88f64) * Add unit test (cherry picked from commit 946710f2e9960b3839613d4bdf730e57ba38a964) * Sovrn adapter updates: schain, digitrust, pixel syncing, and 3.0 upgrades (#4385) * schain and digitrust * pixel beacons * unit tests and fixes from testing * Prebid 3.0 updates * review fix * use backwards compatible flatMap impl * update pixel tests * unit test fix * update one more url to ssl * fixed test * review updates * TheMediaGrid Bid Adapter update (#4447) * Added Grid Bid Adapter * remove priceType from TheMediaGrid Bid Adapter * Add video support in Grid Bid Adapter * Added test parameter for video slot * update Grid Bid Adapter to set size in response bid * Update Grid Bid Adapter to support identical uids in parameters * Fix typo in test file for Grid Bid Adapter * Update The Grid Media Bidder Adapter to send refererInfo.referer as 'u' parameter in ad request * Hotfix for referrer in Grid Bid Adapter * Grid Bid Adapter: added wrapperType and wrappweVersion to the ad request * TheMediaGrid Bid Adapter: added sync url * TheMediaGrid Bid Adapter: added GDPR params to sync url * TheMediaGrid Bid Adapter: added tests for getUserSyncs function * Conversant Bid Adapter adds support for extended ids (#4462) * Adkernel 3.0 compatibility (#4477) * Rubicon Adapter pchain support (#4480) * rubicon pchain support * removed describe.only * Implemented changes required to provide support for video in the IX bidding adapter for Instream and Outstream contexts. (#4424) * Default size filter & KVP support (#4452) * adding DMX test @97%, two files added one updated * Update districtm_spec.js * Update districtmDMX.js * adding all districtm needed file * remove legacy file * remove typo || 0 in the test method * force default to return a valid width and height * update unit test code for failing test * changed class for an object * remove package-lock.json * change file name for dmx adapter * renamed files * restaure package-lock.json * update to last package-lock state * update gdpr user consent * fix sizes issue * Documentation updates Adding the readme.md info * update file name and update unit testing import file location * current machine state * lint correction * remove variable assigment duplicate * adding logic upto5 * adding support for removing and shuffle sizes * adding array split test * re-assign none standard size to the request * resolve duplicate format inside format array * update .md and adaptor file for KVP support * remove array helper includes * inforce two digit after decimal * RUn error check nothing on my side but error form another adapter * add id5id to prebid server bid adapter (#4468) * Added _pbjsGlobals for tracking renames. Resolves #4254 (#4419) * Feature/smart video (#4367) * Adding outstream video support. * Fixing unit test. * Adding video instream support. * Handling video startDelay parameter. * Improving unit tests. * Fixing indent. * Handling the request when videoMediaType context is not supported. * Changing maintainer mail address. * Remove video outstream specific code. * Unit test updated. * do not select element that gets removed after dfp render (#4423) * add smms adapter (#4439) * add smms adapter * re-run ci, why adigo adapter failed?? * review comments fix, remove deprecated functions, fix unit test * Prebid 2.41.0 release * Increment pre version * adds schain param (#4442) * Create newborntownWeb adapter (#4455) * Create newborntownWeb adapter * only https protocol * Provide criteoId to server by user.ext.eids (#4478) * ucfunnel adapter fix error message in debug mode (#4338) * Add a new ucfunnel Adapter and test page * Add a new ucfunnel Adapter and test page * 1. Use prebid lib in the repo to keep updated 2. Replace var with let 3. Put JSON.parse(JSON.stringify()) into try catch block * utils.getTopWindowLocation is a function * Change to modules from adapters * Migrate to module design * [Dev Fix] Remove width and height which can be got from ad unit id * Update ucfunnelBidAdapter to fit into new spec * Correct the endpoint. Fix the error of query string * Add test case for ucfunnelBidAdapter * Fix lint error * Update version number * Combine all checks on bid request * Add GDPR support for ucfunnel adapter * Add in-stream video and native support for ucfunnel adapter * Remove demo page. Add more test cases. * Change request method from POST to GET * Remove unnecessary comment * Support vastXml and vastUrl for video request * update TTL to 30 mins * Avoid using arrow function which is not discuraged in mocha * ucfunnel tdid support * ucfunnel fix error message in debug mode * explicitly check undefined to allow falsey values in getConfig (#4486) * Conversant Bid Adapter handles vast xml (#4492) * [feature] Add a config list of submodules that require refreshing the stored ID after each bid request (#4325) * add a feature to always refresh stored id on each bid request for submodules that require that * update test comments * Prebid 2.42.0 Release * Increment pre version * Make adhese adapter prebid 3.0 compatible (#4507) * Added 'adhese' attribute to bid that contains meta data - Jira AD-2642 * added DALE to adhese determination * extra config option: no format, but use size array as format string * Read sizes from mediaTypes.banner.sizes + Apply Eslint suggestions * Use map and join, add originData to response * properly use originData obj * Remove duplicated ids * Update tests * BugFix: Site id missing (#4467) * outstream changes * removing global filtet * reverting page * message * adapter change * remove space * testcases * testpage * spaces for test page * renderer exist case * reverting package-lock.json * adding schain object * adding tagid * syntaxx error fix * video.html * space trailing * space * tagid * inventoryId and placement * rewarded video * added unit test case * adding site id * adding placement and siteis * site id param test case * removing deprecated functions * correcting test cases * indentation * test cases fix * Remove adtarget from aliases * Undo Co-authored-by: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Co-authored-by: Jozef Bartek <31618107+jbartek25@users.noreply.github.com> Co-authored-by: koji-eguchi <50477903+DAC-KOJI-EGUCHI@users.noreply.github.com> Co-authored-by: Telaria Engineering <36203956+telariaEng@users.noreply.github.com> Co-authored-by: Mike Chowla Co-authored-by: robdubois <53589945+robdubois@users.noreply.github.com> Co-authored-by: sumit116 Co-authored-by: nwlosinski Co-authored-by: Bret Gorsline Co-authored-by: bretg Co-authored-by: Artem Seryak Co-authored-by: Jonathan Mullins Co-authored-by: htang555 Co-authored-by: Bryan DeLong Co-authored-by: dpapworth-qc <50959025+dpapworth-qc@users.noreply.github.com> Co-authored-by: DeepthiNeeladri Co-authored-by: Harshad Mane Co-authored-by: Roman Co-authored-by: Neelanjan Sen <14229985+Fawke@users.noreply.github.com> Co-authored-by: Margaret Liu Co-authored-by: TJ Eastmond Co-authored-by: Robert Ray Martinez III Co-authored-by: Jason Snellbaker Co-authored-by: JonGoSonobi Co-authored-by: Vladimir Fedoseev Co-authored-by: DJ Rosenbaum Co-authored-by: Alex Khmelnitsky Co-authored-by: adxpremium <55161519+adxpremium@users.noreply.github.com> Co-authored-by: Jimmy Tu Co-authored-by: Pierre-Antoine Durgeat Co-authored-by: Eric Harper Co-authored-by: ujuettner Co-authored-by: Dan Bogdan <43830380+EMXDigital@users.noreply.github.com> Co-authored-by: PWyrembak Co-authored-by: susyt Co-authored-by: Max Crawford Co-authored-by: Pascal S Co-authored-by: Will Chapin Co-authored-by: Lemma Dev <54662130+lemmadev@users.noreply.github.com> Co-authored-by: Denis Logachov Co-authored-by: Léonard Labat Co-authored-by: onlsol <48312668+onlsol@users.noreply.github.com> Co-authored-by: Paul Yang Co-authored-by: Matt Kendall <1870166+mkendall07@users.noreply.github.com> Co-authored-by: Mike Sperone Co-authored-by: sdbaron Co-authored-by: djaxbidder <55269794+djaxbidder@users.noreply.github.com> Co-authored-by: turktelssp <54801433+turktelssp@users.noreply.github.com> Co-authored-by: nkmt <45026101+strong-zero@users.noreply.github.com> Co-authored-by: Mutasem Aldmour Co-authored-by: r-schweitzer <50628828+r-schweitzer@users.noreply.github.com> Co-authored-by: Isaac A. Dettman Co-authored-by: Adasta Media <55529969+Adasta2019@users.noreply.github.com> Co-authored-by: mamatic <52153441+mamatic@users.noreply.github.com> Co-authored-by: Konduit <55142865+konduit-dev@users.noreply.github.com> Co-authored-by: TinchoF <50110327+TinchoF@users.noreply.github.com> Co-authored-by: Jaimin Panchal <7393273+jaiminpanchal27@users.noreply.github.com> Co-authored-by: Jaimin Panchal Co-authored-by: Sergio Co-authored-by: Wayne Yang Co-authored-by: Cody Bonney Co-authored-by: evanmsmrtb Co-authored-by: hdeodhar <35999856+hdeodhar@users.noreply.github.com> Co-authored-by: Oz Weiss Co-authored-by: Janko Ulaga Co-authored-by: thomas-33across <44033452+thomas-33across@users.noreply.github.com> Co-authored-by: Finteza Analytics <45741245+finteza@users.noreply.github.com> Co-authored-by: Vadim Mazzherin Co-authored-by: Hendrik Iseke <39734979+hiseke@users.noreply.github.com> Co-authored-by: Anand Venkatraman Co-authored-by: Eyas Ranjous Co-authored-by: Michael Co-authored-by: hbanalytics <55453525+hbanalytics@users.noreply.github.com> Co-authored-by: Index Exchange 3 Prebid Team Co-authored-by: Michael Kuryshev Co-authored-by: Roffray Co-authored-by: rumesh Co-authored-by: oasis <2394426+bmwcmw@users.noreply.github.com> Co-authored-by: Nepomuk Seiler Co-authored-by: John Salis Co-authored-by: OneTagDevOps <38786435+OneTagDevOps@users.noreply.github.com> Co-authored-by: Ankit Prakash Co-authored-by: Dan Co-authored-by: romanantropov <45817046+romanantropov@users.noreply.github.com> Co-authored-by: msm0504 <51493331+msm0504@users.noreply.github.com> Co-authored-by: vrtcal-dev <50931150+vrtcal-dev@users.noreply.github.com> Co-authored-by: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Co-authored-by: colbertk <50499465+colbertk@users.noreply.github.com> Co-authored-by: Steve Alliance Co-authored-by: bjorn-lw <32431346+bjorn-lw@users.noreply.github.com> Co-authored-by: 7XBID00 <52267720+7XBID00@users.noreply.github.com> Co-authored-by: Tomas Kovtun Co-authored-by: Olivier Co-authored-by: Gena Co-authored-by: Jonathan Mullins Co-authored-by: ucfunnel <39581136+ucfunnel@users.noreply.github.com> Co-authored-by: ACannuniRP <57228257+ACannuniRP@users.noreply.github.com> Co-authored-by: Hugo Duthil Co-authored-by: skazedo Co-authored-by: 胡雨軒 Петр Co-authored-by: Konrad Dulemba Co-authored-by: Mariya Mego <31904600+mash-a@users.noreply.github.com> Co-authored-by: Daniel Cassidy Co-authored-by: kpis-msa <50609476+kpis-msa@users.noreply.github.com> Co-authored-by: omerBrowsi <54346241+omerBrowsi@users.noreply.github.com> Co-authored-by: Marcian123 Co-authored-by: koji-eguchi <50477903+koji-eguchi@users.noreply.github.com> Co-authored-by: sourabhg Co-authored-by: Alexis Andrieu Co-authored-by: Vlad Gurgov Co-authored-by: Richard Lee <14349+dlackty@users.noreply.github.com> Co-authored-by: Alex Co-authored-by: Vladislav Yatsun Co-authored-by: vincentproxistore <56686565+vincentproxistore@users.noreply.github.com> Co-authored-by: Rich Snapp Co-authored-by: Rade Popovic <32302052+nanointeractive@users.noreply.github.com> Co-authored-by: Matt Quirion Co-authored-by: rhythmonebhaines <49991465+rhythmonebhaines@users.noreply.github.com> Co-authored-by: binoy-chitale Co-authored-by: Alex Pashkov Co-authored-by: harpere Co-authored-by: Scott Co-authored-by: tadam75 Co-authored-by: Veronica Kim <43146383+vkimcm@users.noreply.github.com> Co-authored-by: songtungmtp <57524426+songtungmtp@users.noreply.github.com> Co-authored-by: z-sunshine <33084773+z-sunshine@users.noreply.github.com> Co-authored-by: Sander Co-authored-by: Moshe Moses --- modules/gamoshiBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/gamoshiBidAdapter.js b/modules/gamoshiBidAdapter.js index e052e31cf40..1316d74e430 100644 --- a/modules/gamoshiBidAdapter.js +++ b/modules/gamoshiBidAdapter.js @@ -42,7 +42,7 @@ export const helper = { export const spec = { code: 'gamoshi', - aliases: ['gambid', 'cleanmedia', '9MediaOnline', 'adtarget'], + aliases: ['gambid', 'cleanmedia', '9MediaOnline'], supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { From 309c16580b0a39c2954483e632856fa06f7bca20 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 6 Apr 2020 08:16:46 -0700 Subject: [PATCH 58/90] Pubmatic: setting seat-id and dspid in bid object (#5074) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * PubMatic: setting pm_seat and pm_dspid in bid response * default value for pm_dspid is set to null --- modules/pubmaticBidAdapter.js | 4 +++- test/spec/modules/pubmaticBidAdapter_spec.js | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 605ee69603f..49f193cf93e 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -990,7 +990,9 @@ export const spec = { netRevenue: NET_REVENUE, ttl: 300, referrer: parsedReferrer, - ad: bid.adm + ad: bid.adm, + pm_seat: seatbidder.seat || null, + pm_dspid: bid.ext && bid.ext.dspid ? bid.ext.dspid : null }; if (parsedRequest.imp && parsedRequest.imp.length > 0) { parsedRequest.imp.forEach(req => { diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 62dd76f04b6..36b984296fc 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -533,6 +533,7 @@ describe('PubMatic adapter', function () { 'body': { 'id': '93D3BAD6-E2E2-49FB-9D89-920B1761C865', 'seatbid': [{ + 'seat': 'seat-id', 'bid': [{ 'id': '74858439-49D7-4169-BA5D-44A046315B2F', 'impid': '22bddb28db77d', @@ -2561,6 +2562,8 @@ describe('PubMatic adapter', function () { expect(response[0].meta.clickUrl).to.equal('blackrock.com'); expect(response[0].referrer).to.include(data.site.ref); expect(response[0].ad).to.equal(bidResponses.body.seatbid[0].bid[0].adm); + expect(response[0].pm_seat).to.equal(bidResponses.body.seatbid[0].seat); + expect(response[0].pm_dspid).to.equal(bidResponses.body.seatbid[0].bid[0].ext.dspid); expect(response[1].requestId).to.equal(bidResponses.body.seatbid[1].bid[0].impid); expect(response[1].cpm).to.equal((bidResponses.body.seatbid[1].bid[0].price).toFixed(2)); @@ -2580,6 +2583,8 @@ describe('PubMatic adapter', function () { expect(response[1].meta.clickUrl).to.equal('hivehome.com'); expect(response[1].referrer).to.include(data.site.ref); expect(response[1].ad).to.equal(bidResponses.body.seatbid[1].bid[0].adm); + expect(response[1].pm_seat).to.equal(bidResponses.body.seatbid[1].seat || null); + expect(response[1].pm_dspid).to.equal(bidResponses.body.seatbid[1].bid[0].ext.dspid); }); it('should check for dealChannel value selection', function () { From 59ccbbf7452c110e4558a73b97ecda601e57d2f5 Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Mon, 6 Apr 2020 08:54:08 -0700 Subject: [PATCH 59/90] Fix bug in rubicon bid adapter for fpd keywords (#5078) Co-authored-by: rmartinez --- modules/rubiconBidAdapter.js | 2 +- test/spec/modules/rubiconBidAdapter_spec.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 616a17f470f..9131a8cb581 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -575,7 +575,7 @@ export const spec = { const keywords = (params.keywords || []).concat( utils.deepAccess(config.getConfig('fpd.user'), 'keywords') || [], utils.deepAccess(config.getConfig('fpd.context'), 'keywords') || []); - data.kw = keywords.length ? keywords.join(',') : ''; + data.kw = Array.isArray(keywords) && keywords.length ? keywords.join(',') : ''; /** * Prebid AdSlot diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 4e999f4bdab..29cc6cb2c0a 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1924,6 +1924,12 @@ describe('the rubicon adapter', function () { } }); }); + + it('should not fail if keywords param is not an array', function () { + bidderRequest.bids[0].params.keywords = 'a,b,c'; + const slotParams = spec.createSlotParams(bidderRequest.bids[0], bidderRequest); + expect(slotParams.kw).to.equal(''); + }); }); describe('hasVideoMediaType', function () { From 753fcc848be3331cb32a6abe7256696af46cf228 Mon Sep 17 00:00:00 2001 From: Denis Logachov Date: Mon, 6 Apr 2020 19:52:16 +0300 Subject: [PATCH 60/90] Native support for AdKernel adapter (#5045) * Native support for adkernel adapter * Updated doc --- modules/adkernelBidAdapter.js | 136 ++++++++++++++++- modules/adkernelBidAdapter.md | 98 +++++++----- test/spec/modules/adkernelBidAdapter_spec.js | 150 ++++++++++++++++--- 3 files changed, 326 insertions(+), 58 deletions(-) diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 18c60c3e438..dbb0f4a3344 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -1,5 +1,5 @@ import * as utils from '../src/utils.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import find from 'core-js/library/fn/array/find.js'; import includes from 'core-js/library/fn/array/includes.js'; @@ -15,7 +15,30 @@ import {parse as parseUrl} from '../src/url.js'; const VIDEO_TARGETING = ['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'linearity', 'boxingallowed', 'playbackmethod', 'delivery', 'pos', 'api', 'ext']; -const VERSION = '1.3'; +const VERSION = '1.4'; + +const NATIVE_MODEL = [ + {name: 'title', assetType: 'title'}, + {name: 'icon', assetType: 'img', type: 1}, + {name: 'image', assetType: 'img', type: 3}, + {name: 'body', assetType: 'data', type: 2}, + {name: 'body2', assetType: 'data', type: 10}, + {name: 'sponsoredBy', assetType: 'data', type: 1}, + {name: 'phone', assetType: 'data', type: 8}, + {name: 'address', assetType: 'data', type: 9}, + {name: 'price', assetType: 'data', type: 6}, + {name: 'salePrice', assetType: 'data', type: 7}, + {name: 'cta', assetType: 'data', type: 12}, + {name: 'rating', assetType: 'data', type: 3}, + {name: 'downloads', assetType: 'data', type: 5}, + {name: 'likes', assetType: 'data', type: 4}, + {name: 'displayUrl', assetType: 'data', type: 11} +]; + +const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => { + acc[val.name] = {id: idx, ...val}; + return acc; +}, {}); /** * Adapter for requesting bids from AdKernel white-label display platform @@ -24,7 +47,7 @@ export const spec = { code: 'adkernel', aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon'], - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: function(bidRequest) { return 'params' in bidRequest && typeof bidRequest.params.host !== 'undefined' && @@ -32,7 +55,7 @@ export const spec = { !isNaN(Number(bidRequest.params.zoneId)) && bidRequest.params.zoneId > 0 && bidRequest.mediaTypes && - (bidRequest.mediaTypes.banner || bidRequest.mediaTypes.video); + (bidRequest.mediaTypes.banner || bidRequest.mediaTypes.video || (bidRequest.mediaTypes.native && validateNativeAdUnit(bidRequest.mediaTypes.native))); }, buildRequests: function(bidRequests, bidderRequest) { let impDispatch = dispatchImps(bidRequests, bidderRequest.refererInfo); @@ -76,12 +99,14 @@ export const spec = { prBid.width = rtbBid.w; prBid.height = rtbBid.h; prBid.ad = formatAdMarkup(rtbBid); - } - if ('video' in imp) { + } else if ('video' in imp) { prBid.mediaType = VIDEO; prBid.vastUrl = rtbBid.nurl; prBid.width = imp.video.w; prBid.height = imp.video.h; + } else if ('native' in imp) { + prBid.mediaType = NATIVE; + prBid.native = buildNativeAd(JSON.parse(rtbBid.adm)); } return prBid; }); @@ -138,6 +163,12 @@ function buildImp(bidRequest, secure) { .filter(key => includes(VIDEO_TARGETING, key)) .forEach(key => imp.video[key] = bidRequest.params.video[key]); } + } else if (utils.deepAccess(bidRequest, 'mediaTypes.native')) { + let nativeRequest = buildNativeRequest(bidRequest.mediaTypes.native); + imp.native = { + ver: '1.1', + request: JSON.stringify(nativeRequest) + } } if (secure) { imp.secure = 1; @@ -145,6 +176,51 @@ function buildImp(bidRequest, secure) { return imp; } +/** + * Builds native request from native adunit + */ +function buildNativeRequest(nativeReq) { + let request = {ver: '1.1', assets: []}; + for (let k of Object.keys(nativeReq)) { + let v = nativeReq[k]; + let desc = NATIVE_INDEX[k]; + if (desc === undefined) { + continue; + } + let assetRoot = { + id: desc.id, + required: ~~v.required, + }; + if (desc.assetType === 'img') { + assetRoot[desc.assetType] = buildImageAsset(desc, v); + } else if (desc.assetType === 'data') { + assetRoot.data = utils.cleanObj({type: desc.type, len: v.len}); + } else if (desc.assetType === 'title') { + assetRoot.title = {len: v.len || 90}; + } else { + return; + } + request.assets.push(assetRoot); + } + return request; +} + +/** + * Builds image asset request + */ +function buildImageAsset(desc, val) { + let img = { + type: desc.type + }; + if (val.sizes) { + [img.w, img.h] = val.sizes; + } else if (val.aspect_ratios) { + img.wmin = val.aspect_ratios[0].min_width; + img.hmin = val.aspect_ratios[0].min_height; + } + return utils.cleanObj(img); +} + /** * Builds complete rtb request * @param imps collection of impressions @@ -224,3 +300,51 @@ function formatAdMarkup(bid) { } return adm; } + +/** + * Basic validates to comply with platform requirements + */ +function validateNativeAdUnit(adUnit) { + return validateNativeImageSize(adUnit.image) && validateNativeImageSize(adUnit.icon) && + !utils.deepAccess(adUnit, 'privacyLink.required') && // not supported yet + !utils.deepAccess(adUnit, 'privacyIcon.required'); // not supported yet +} + +/** + * Validates image asset size definition + */ +function validateNativeImageSize(img) { + if (!img) { + return true; + } + if (img.sizes) { + return utils.isArrayOfNums(img.sizes, 2); + } + if (utils.isArray(img.aspect_ratios)) { + return img.aspect_ratios.length > 0 && img.aspect_ratios[0].min_height && img.aspect_ratios[0].min_width; + } + return true; +} + +/** + * Creates native ad for native 1.1 response + */ +function buildNativeAd(nativeResp) { + const {assets, link, imptrackers, jstracker, privacy} = nativeResp.native; + let nativeAd = { + clickUrl: link.url, + impressionTrackers: imptrackers, + javascriptTrackers: jstracker ? [jstracker] : undefined, + privacyLink: privacy, + }; + utils._each(assets, asset => { + let assetName = NATIVE_MODEL[asset.id].name; + let assetType = NATIVE_MODEL[asset.id].assetType; + nativeAd[assetName] = asset[assetType].text || asset[assetType].value || utils.cleanObj({ + url: asset[assetType].url, + width: asset[assetType].w, + height: asset[assetType].h + }); + }); + return utils.cleanObj(nativeAd); +} diff --git a/modules/adkernelBidAdapter.md b/modules/adkernelBidAdapter.md index f89fa5a26df..b3854d721ad 100644 --- a/modules/adkernelBidAdapter.md +++ b/modules/adkernelBidAdapter.md @@ -9,43 +9,71 @@ Maintainer: prebid-dev@adkernel.com # Description Connects to AdKernel whitelabel platform. -Banner and video formats are supported. +Banner, video and native formats are supported. # Test Parameters ``` - var adUnits = [{ - code: 'banner-ad-div', - mediaTypes: { - banner: { - sizes: [[300, 250]], // banner size - } - }, - bids: [ - { - bidder: 'adkernel', - params: { - zoneId: '30164', //required parameter - host: 'cpm.metaadserving.com' //required parameter - } - } - ] - }, { - code: 'video-ad-player', - mediaTypes: { - video: { - context: 'instream', // or 'outstream' - playerSize: [640, 480] // video player size - } - }, - bids: [ - { - bidder: 'adkernel', - params: { - zoneId: '30164', //required parameter - host: 'cpm.metaadserving.com' //required parameter - } - } - ] - }]; +var adUnits = [{ + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], // banner size + } + }, + bids: [{ + bidder: 'adkernel', + params: { + zoneId: '30164', //required parameter + host: 'cpm.metaadserving.com' //required parameter + } + }] +}, { + code: 'video-ad-player', + mediaTypes: { + video: { + context: 'instream', // or 'outstream' + playerSize: [640, 480] // video player size + } + }, + bids: [{ + bidder: 'adkernel', + params: { + zoneId: '30164', //required parameter + host: 'cpm.metaadserving.com' //required parameter + } + }] +}, { + code: 'native-ad', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + displayUrl: { + required: false + } + } + }, + bids: [{ + bidder: 'adkernel', + params: { + host: 'cpm.metaadserving.com', + zoneId: 30164 + } + }] +}]; ``` diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 3ae37592f1e..ef95febf13d 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from 'modules/adkernelBidAdapter.js'; import * as utils from 'src/utils.js'; +import {NATIVE, BANNER, VIDEO} from 'src/mediaTypes'; describe('Adkernel adapter', function () { const bid1_zone1 = { @@ -105,6 +106,57 @@ describe('Adkernel adapter', function () { bidId: 'Bid_01', bidderRequestId: 'req-001', auctionId: 'auc-001' + }, bid_native = { + bidder: 'adkernel', + params: {zoneId: 1, host: 'rtb.adkernel.com'}, + mediaTypes: { + native: { + title: { + required: true, + len: 80 + }, + body: { + required: true + }, + body2: { + required: true + }, + icon: { + required: true, + aspect_ratios: [{min_width: 50, min_height: 50}] + }, + image: { + required: true, + sizes: [300, 200] + }, + clickUrl: { + required: true + }, + rating: { + required: false + }, + price: { + required: false + }, + privacyLink: { + required: false + }, + cta: { + required: false + }, + sponsoredBy: { + required: false + }, + displayUrl: { + required: false + } + } + }, + adUnitCode: 'ad-unit-1', + transactionId: 'f82c64b8-c602-42a4-9791-4a268f6559ed', + bidId: 'Bid_01', + bidderRequestId: 'req-001', + auctionId: 'auc-001' }; const bidResponse1 = { @@ -125,20 +177,6 @@ describe('Adkernel adapter', function () { ext: { adk_usersync: ['https://adk.sync.com/sync'] } - }, bidResponse2 = { - id: 'bid2', - seatbid: [{ - bid: [{ - id: '2', - impid: 'Bid_02', - crid: '100_002', - price: 1.31, - adm: '', - w: 300, - h: 250 - }] - }], - cur: 'USD' }, videoBidResponse = { id: '47ce4badcf7482', seatbid: [{ @@ -158,6 +196,36 @@ describe('Adkernel adapter', function () { ext: { adk_usersync: ['https://adk.sync.com/sync'] } + }, nativeResponse = { + id: '56fbc713-b737-4651-9050-13376aed9818', + seatbid: [{ + bid: [{ + id: 'someid_01', + impid: 'Bid_01', + price: 2.25, + adid: '4', + adm: JSON.stringify({ + native: { + assets: [ + {id: 0, title: {text: 'Title'}}, + {id: 3, data: {value: 'Description'}}, + {id: 4, data: {value: 'Additional description'}}, + {id: 1, img: {url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0&imgt=icon', w: 50, h: 50}}, + {id: 2, img: {url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0', w: 300, h: 200}}, + {id: 5, data: {value: 'Sponsor.com'}}, + {id: 14, data: {value: 'displayurl.com'}} + ], + link: {url: 'http://rtb.com/click?i=pTuOlf5KHUo_0'}, + imptrackers: ['http://rtb.com/win?i=pTuOlf5KHUo_0&f=imp'] + } + }), + adomain: ['displayurl.com'], + cid: '1', + crid: '4' + }] + }], + bidid: 'pTuOlf5KHUo', + cur: 'USD' }; function buildBidderRequest(url = 'https://example.com/index.html', params = {}) { @@ -191,6 +259,10 @@ describe('Adkernel adapter', function () { it('empty request shouldn\'t generate exception', function () { expect(spec.isBidRequestValid(bid_with_wrong_zoneId)).to.be.equal(false); }); + + it('valid native requests should pass', () => { + expect(spec.isBidRequestValid(bid_native)).to.be.equal(true); + }) }); describe('banner request building', function () { @@ -270,7 +342,6 @@ describe('Adkernel adapter', function () { it('should forward default bidder timeout', function() { let [_, bidRequests] = buildRequest([bid1_zone1], DEFAULT_BIDDER_REQUEST); - let bidRequest = bidRequests[0]; expect(bidRequests[0]).to.have.property('tmax', 3000); }); }); @@ -343,7 +414,7 @@ describe('Adkernel adapter', function () { expect(resp).to.have.property('creativeId', '100_001'); expect(resp).to.have.property('currency'); expect(resp).to.have.property('ttl'); - expect(resp).to.have.property('mediaType', 'banner'); + expect(resp).to.have.property('mediaType', BANNER); expect(resp).to.have.property('ad'); expect(resp.ad).to.have.string(''); }); @@ -352,7 +423,7 @@ describe('Adkernel adapter', function () { let [pbRequests, _] = buildRequest([bid_video]); let resp = spec.interpretResponse({body: videoBidResponse}, pbRequests[0])[0]; expect(resp).to.have.property('requestId', 'Bid_Video'); - expect(resp.mediaType).to.equal('video'); + expect(resp.mediaType).to.equal(VIDEO); expect(resp.cpm).to.equal(0.00145); expect(resp.vastUrl).to.equal('https://rtb.com/win?i=sZSYq5zYMxo_0&f=nurl'); expect(resp.width).to.equal(640); @@ -388,4 +459,49 @@ describe('Adkernel adapter', function () { expect(spec.aliases).to.include.members(['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon']); }); }); + + describe('native support', () => { + let _, bidRequests; + before(function () { + [_, bidRequests] = buildRequest([bid_native]); + }); + + it('native request building', () => { + expect(bidRequests[0].imp).to.have.length(1); + expect(bidRequests[0].imp[0]).to.have.property('native'); + expect(bidRequests[0].imp[0].native).to.have.property('request'); + let request = JSON.parse(bidRequests[0].imp[0].native.request); + expect(request).to.have.property('ver', '1.1'); + expect(request.assets).to.have.length(10); + expect(request.assets[0]).to.be.eql({id: 0, required: 1, title: {len: 80}}); + expect(request.assets[1]).to.be.eql({id: 3, required: 1, data: {type: 2}}); + expect(request.assets[2]).to.be.eql({id: 4, required: 1, data: {type: 10}}); + expect(request.assets[3]).to.be.eql({id: 1, required: 1, img: {wmin: 50, hmin: 50, type: 1}}); + expect(request.assets[4]).to.be.eql({id: 2, required: 1, img: {w: 300, h: 200, type: 3}}); + expect(request.assets[5]).to.be.eql({id: 11, required: 0, data: {type: 3}}); + expect(request.assets[6]).to.be.eql({id: 8, required: 0, data: {type: 6}}); + expect(request.assets[7]).to.be.eql({id: 10, required: 0, data: {type: 12}}); + expect(request.assets[8]).to.be.eql({id: 5, required: 0, data: {type: 1}}); + expect(request.assets[9]).to.be.eql({id: 14, required: 0, data: {type: 11}}); + }); + + it('native response processing', () => { + let [pbRequests, _] = buildRequest([bid_native]); + let resp = spec.interpretResponse({body: nativeResponse}, pbRequests[0])[0]; + expect(resp).to.have.property('requestId', 'Bid_01'); + expect(resp).to.have.property('cpm', 2.25); + expect(resp).to.have.property('currency', 'USD'); + expect(resp).to.have.property('mediaType', NATIVE); + expect(resp).to.have.property('native'); + expect(resp.native).to.have.property('clickUrl', 'http://rtb.com/click?i=pTuOlf5KHUo_0'); + expect(resp.native.impressionTrackers).to.be.eql(['http://rtb.com/win?i=pTuOlf5KHUo_0&f=imp']); + expect(resp.native).to.have.property('title', 'Title'); + expect(resp.native).to.have.property('body', 'Description'); + expect(resp.native).to.have.property('body2', 'Additional description'); + expect(resp.native.icon).to.be.eql({url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0&imgt=icon', width: 50, height: 50}); + expect(resp.native.image).to.be.eql({url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0', width: 300, height: 200}); + expect(resp.native).to.have.property('sponsoredBy', 'Sponsor.com'); + expect(resp.native).to.have.property('displayUrl', 'displayurl.com'); + }); + }); }); From b297edf5afa74c38269010f31410757a8ccd63e2 Mon Sep 17 00:00:00 2001 From: bidlab <61749467+bidlab@users.noreply.github.com> Date: Tue, 7 Apr 2020 01:07:41 +0300 Subject: [PATCH 61/90] New bidlab prebid adapter (#5020) * initial * fix * initial --- modules/bidlabBidAdapter.js | 112 ++++++++++ modules/bidlabBidAdapter.md | 53 +++++ test/spec/modules/bidlabBidAdapter_spec.js | 235 +++++++++++++++++++++ 3 files changed, 400 insertions(+) create mode 100644 modules/bidlabBidAdapter.js create mode 100644 modules/bidlabBidAdapter.md create mode 100644 test/spec/modules/bidlabBidAdapter_spec.js diff --git a/modules/bidlabBidAdapter.js b/modules/bidlabBidAdapter.js new file mode 100644 index 00000000000..8f501505a6d --- /dev/null +++ b/modules/bidlabBidAdapter.js @@ -0,0 +1,112 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const BIDDER_CODE = 'bidlab'; +const AD_URL = 'https://service.bidlab.ai/?c=o&m=multi'; +const URL_SYNC = 'https://service.bidlab.ai/?c=o&m=sync'; +const NO_SYNC = true; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency) { + return false; + } + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl); + case NATIVE: + return Boolean(bid.native && bid.native.title && bid.native.image && bid.native.impressionTrackers); + default: + return false; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + noSync: NO_SYNC, + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.placementId))); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + let winTop = window; + let location; + try { + location = new URL(bidderRequest.refererInfo.referer) + winTop = window.top; + } catch (e) { + location = winTop.location; + utils.logMessage(e); + }; + let placements = []; + let request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + 'secure': 1, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent + } + } + const len = validBidRequests.length; + + for (let i = 0; i < len; i++) { + let bid = validBidRequests[i]; + let traff = bid.params.traffic || BANNER + + placements.push({ + placementId: bid.params.placementId, + bidId: bid.bidId, + sizes: bid.mediaTypes && bid.mediaTypes[traff] && bid.mediaTypes[traff].sizes ? bid.mediaTypes[traff].sizes : [], + traffic: traff + }); + if (bid.schain) { + placements.schain = bid.schain; + } + } + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses) => { + if (NO_SYNC) { + return false + } else { + return [{ + type: 'image', + url: URL_SYNC + }]; + } + } + +}; + +registerBidder(spec); diff --git a/modules/bidlabBidAdapter.md b/modules/bidlabBidAdapter.md new file mode 100644 index 00000000000..3e5fe3128ed --- /dev/null +++ b/modules/bidlabBidAdapter.md @@ -0,0 +1,53 @@ +# Overview + +``` +Module Name: bidlab Bidder Adapter +Module Type: bidlab Bidder Adapter +``` + +# Description + +Module that connects to bidlab demand sources + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'placementId_0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'bidlab', + params: { + placementId: 0, + traffic: 'banner' + } + } + ] + }, + // Will return test vast xml. All video params are stored under placement in publishers UI + { + code: 'placementId_0', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'bidlab', + params: { + placementId: 0, + traffic: 'video' + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/bidlabBidAdapter_spec.js b/test/spec/modules/bidlabBidAdapter_spec.js new file mode 100644 index 00000000000..cffd43ae6ca --- /dev/null +++ b/test/spec/modules/bidlabBidAdapter_spec.js @@ -0,0 +1,235 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/bidlabBidAdapter.js'; + +describe('BidlabBidAdapter', function () { + let bid = { + bidId: '23fhj33i987f', + bidder: 'bidlab', + params: { + placementId: 0, + traffic: 'banner' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and placementId parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://service.bidlab.ai/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes'); + expect(placement.placementId).to.equal(0); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal('banner'); + }); + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + describe('getUserSyncs', function () { + let userSync = spec.getUserSyncs(); + it('Returns valid URL and type', function () { + if (spec.noSync) { + expect(userSync).to.be.equal(false); + } else { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync[0].type).to.be.equal('image'); + expect(userSync[0].url).to.be.equal('https://service.bidlab.ai/?c=o&m=sync'); + } + }); + }); +}); From 4def1cf5bbc3d6be46469a6f9096f72e6b65cc50 Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Mon, 6 Apr 2020 15:09:31 -0700 Subject: [PATCH 62/90] Surrounding adapter calls in try (#4915) * Surrounding adapter calls in try Several test flakyness updates * adding test * const instead of let Co-authored-by: rmartinez --- src/adapterManager.js | 32 +++++++++------ .../modules/adomikAnalyticsAdapter_spec.js | 9 +++- test/spec/unit/core/adapterManager_spec.js | 41 +++++++++++++++++++ test/spec/unit/pbjs_api_spec.js | 8 +++- 4 files changed, 73 insertions(+), 17 deletions(-) diff --git a/src/adapterManager.js b/src/adapterManager.js index 9886934ab47..f658a36aeda 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -368,19 +368,25 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request request: requestCallbacks.request.bind(null, bidRequest.bidderCode), done: requestCallbacks.done } : undefined); - config.runWithBidder( - bidRequest.bidderCode, - bind.call( - adapter.callBids, - adapter, - bidRequest, - addBidResponse.bind(bidRequest), - doneCb.bind(bidRequest), - ajax, - onTimelyResponse, - config.callbackWithBidder(bidRequest.bidderCode) - ) - ); + const adapterDone = doneCb.bind(bidRequest); + try { + config.runWithBidder( + bidRequest.bidderCode, + bind.call( + adapter.callBids, + adapter, + bidRequest, + addBidResponse.bind(bidRequest), + adapterDone, + ajax, + onTimelyResponse, + config.callbackWithBidder(bidRequest.bidderCode) + ) + ); + } catch (e) { + utils.logError(`${bidRequest.bidderCode} Bid Adapter emitted an uncaught error when parsing their bidRequest`, {e, bidRequest}); + adapterDone(); + } }); }; diff --git a/test/spec/modules/adomikAnalyticsAdapter_spec.js b/test/spec/modules/adomikAnalyticsAdapter_spec.js index a067ce2770e..1414b2402b9 100644 --- a/test/spec/modules/adomikAnalyticsAdapter_spec.js +++ b/test/spec/modules/adomikAnalyticsAdapter_spec.js @@ -7,6 +7,13 @@ let constants = require('src/constants.json'); describe('Adomik Prebid Analytic', function () { let sendEventStub; let sendWonEventStub; + let clock; + before(function () { + clock = sinon.useFakeTimers(); + }); + after(function () { + clock.restore(); + }); describe('enableAnalytics', function () { beforeEach(function () { @@ -120,7 +127,6 @@ describe('Adomik Prebid Analytic', function () { expect(adomikAnalytics.currentContext.timeouted).to.equal(true); // Step 7: Send auction end event - var clock = sinon.useFakeTimers(); events.emit(constants.EVENTS.AUCTION_END, {}); setTimeout(function() { @@ -130,7 +136,6 @@ describe('Adomik Prebid Analytic', function () { }, 3000); clock.tick(5000); - clock.restore(); sinon.assert.callCount(adomikAnalytics.track, 6); }); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 566ebe2e400..29bba0ec0a0 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -43,16 +43,23 @@ var rubiconAdapterMock = { callBids: sinon.stub() }; +var badAdapterMock = { + bidder: 'badBidder', + callBids: sinon.stub().throws(Error('some fake error')) +}; + describe('adapterManager tests', function () { let orgAppnexusAdapter; let orgAdequantAdapter; let orgPrebidServerAdapter; let orgRubiconAdapter; + let orgBadBidderAdapter; before(function () { orgAppnexusAdapter = adapterManager.bidderRegistry['appnexus']; orgAdequantAdapter = adapterManager.bidderRegistry['adequant']; orgPrebidServerAdapter = adapterManager.bidderRegistry['prebidServer']; orgRubiconAdapter = adapterManager.bidderRegistry['rubicon']; + orgBadBidderAdapter = adapterManager.bidderRegistry['badBidder']; }); after(function () { @@ -60,6 +67,7 @@ describe('adapterManager tests', function () { adapterManager.bidderRegistry['adequant'] = orgAdequantAdapter; adapterManager.bidderRegistry['prebidServer'] = orgPrebidServerAdapter; adapterManager.bidderRegistry['rubicon'] = orgRubiconAdapter; + adapterManager.bidderRegistry['badBidder'] = orgBadBidderAdapter; config.setConfig({s2sConfig: { enabled: false }}); }); @@ -72,11 +80,16 @@ describe('adapterManager tests', function () { sinon.stub(utils, 'logError'); appnexusAdapterMock.callBids.reset(); adapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; + adapterManager.bidderRegistry['rubicon'] = rubiconAdapterMock; + adapterManager.bidderRegistry['badBidder'] = badAdapterMock; }); afterEach(function () { utils.logError.restore(); delete adapterManager.bidderRegistry['appnexus']; + delete adapterManager.bidderRegistry['rubicon']; + delete adapterManager.bidderRegistry['badBidder']; + config.resetConfig(); }); it('should log an error if a bidder is used that does not exist', function () { @@ -95,6 +108,34 @@ describe('adapterManager tests', function () { sinon.assert.called(utils.logError); }); + it('should catch a bidder adapter thrown error and continue with other bidders', function () { + const adUnits = [{ + code: 'adUnit-code', + sizes: [[728, 90]], + bids: [ + {bidder: 'appnexus', params: {placementId: 'id'}}, + {bidder: 'badBidder', params: {placementId: 'id'}}, + {bidder: 'rubicon', params: {account: 1111, site: 2222, zone: 3333}} + ] + }]; + let bidRequests = adapterManager.makeBidRequests(adUnits, 1111, 2222, 1000); + + let doneBidders = []; + function mockDoneCB() { + doneBidders.push(this.bidderCode) + } + adapterManager.callBids(adUnits, bidRequests, () => {}, mockDoneCB); + sinon.assert.calledOnce(appnexusAdapterMock.callBids); + sinon.assert.calledOnce(badAdapterMock.callBids); + sinon.assert.calledOnce(rubiconAdapterMock.callBids); + + expect(utils.logError.calledOnce).to.be.true; + expect(utils.logError.calledWith( + 'badBidder Bid Adapter emitted an uncaught error when parsing their bidRequest' + )).to.be.true; + // done should be called for our bidder! + expect(doneBidders.indexOf('badBidder') === -1).to.be.false; + }); it('should emit BID_REQUESTED event', function () { // function to count BID_REQUESTED events let cnt = 0; diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index cfdd39239d3..252c074cd61 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1223,6 +1223,12 @@ describe('Unit: Prebid Module', function () { let makeRequestsStub; let adUnits; let clock; + before(function () { + clock = sinon.useFakeTimers(); + }); + after(function () { + clock.restore(); + }); let bidsBackHandlerStub = sinon.stub(); const BIDDER_CODE = 'sampleBidder'; @@ -1309,7 +1315,6 @@ describe('Unit: Prebid Module', function () { spec.isBidRequestValid.returns(true); spec.interpretResponse.returns(bids); - clock = sinon.useFakeTimers(); let requestObj = { bidsBackHandler: null, // does not need to be defined because of newAuction mock in beforeEach timeout: 2000, @@ -1363,7 +1368,6 @@ describe('Unit: Prebid Module', function () { auction.getBidsReceived = function() { return [adResponse]; } auction.getAuctionId = () => auctionId; - clock = sinon.useFakeTimers(); let requestObj = { bidsBackHandler: null, // does not need to be defined because of newAuction mock in beforeEach timeout: 2000, From 3dfc7384402d3d70dc0d1014e2ec0a32c1e472c9 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 7 Apr 2020 10:16:05 +0200 Subject: [PATCH 63/90] AdagiobidAdapter 2.2.0: schain, tcf 2.0 (#5059) * adagioBidAdapter: add schain support * adagioBidAdapter: bump version 2.2.0 * AdagioBidAdapter: send GDPR api version in request --- modules/adagioBidAdapter.js | 13 ++- test/spec/modules/adagioBidAdapter_spec.js | 97 +++++++++++++++++++++- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 8b593c34270..484371261e3 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -7,7 +7,7 @@ import sha256 from 'crypto-js/sha256.js'; import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'adagio'; -const VERSION = '2.1.0'; +const VERSION = '2.2.0'; const FEATURES_VERSION = '1'; const ENDPOINT = 'https://mp.4dex.io/prebid'; const SUPPORTED_MEDIA_TYPES = ['banner']; @@ -338,10 +338,19 @@ function _getGdprConsent(bidderRequest) { if (bidderRequest.gdprConsent.allowAuctionWithoutConsent !== undefined) { consent.allowAuctionWithoutConsent = bidderRequest.gdprConsent.allowAuctionWithoutConsent ? 1 : 0; } + if (bidderRequest.gdprConsent.apiVersion !== undefined) { + consent.apiVersion = bidderRequest.gdprConsent.apiVersion; + } } return consent; } +function _getSchain(bidRequest) { + if (utils.deepAccess(bidRequest, 'schain')) { + return bidRequest.schain; + } +} + export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -394,6 +403,7 @@ export const spec = { const site = _getSite(); const pageviewId = _getPageviewId(); const gdprConsent = _getGdprConsent(bidderRequest); + const schain = _getSchain(validBidRequests[0]); const adUnits = utils._map(validBidRequests, (bidRequest) => { bidRequest.features = _getFeatures(bidRequest); return bidRequest; @@ -422,6 +432,7 @@ export const spec = { pageviewId: pageviewId, adUnits: groupedAdUnits[organizationId], gdpr: gdprConsent, + schain: schain, prebidVersion: '$prebid.version$', adapterVersion: VERSION, featuresVersion: FEATURES_VERSION diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index b49a591d20d..f387b7a021c 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -7,7 +7,7 @@ describe('adagioAdapter', () => { let utilsMock; const adapter = newBidder(spec); const ENDPOINT = 'https://mp.4dex.io/prebid'; - const VERSION = '2.1.0'; + const VERSION = '2.2.0'; beforeEach(function() { localStorage.removeItem('adagioScript'); @@ -301,7 +301,29 @@ describe('adagioAdapter', () => { 'gdprConsent': { consentString: consentString, gdprApplies: true, - allowAuctionWithoutConsent: true + allowAuctionWithoutConsent: true, + apiVersion: 1, + }, + 'refererInfo': { + 'numIframes': 0, + 'reachedTop': true, + 'referer': 'http://test.io/index.html?pbjs_debug=true' + } + }; + + let bidderRequestTCF2 = { + 'bidderCode': 'adagio', + 'auctionId': '12jejebn', + 'bidderRequestId': 'hehehehbeheh', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + vendorData: { + tcString: consentString, + gdprApplies: true + }, + gdprApplies: true, + apiVersion: 2 }, 'refererInfo': { 'numIframes': 0, @@ -433,6 +455,17 @@ describe('adagioAdapter', () => { expect(request.data.gdpr).to.exist; expect(request.data.gdpr.consentString).to.exist.and.to.equal(consentString); expect(request.data.gdpr.consentRequired).to.exist.and.to.equal(1); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(1); + }); + + it('GDPR consent is applied w/ TCF2', () => { + const requests = spec.buildRequests(bidRequests, bidderRequestTCF2); + expect(requests).to.have.lengthOf(2); + const request = requests[0]; + expect(request.data.gdpr).to.exist; + expect(request.data.gdpr.consentString).to.exist.and.to.equal(consentString); + expect(request.data.gdpr.consentRequired).to.exist.and.to.equal(1); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(2); }); it('GDPR consent is not applied', () => { @@ -443,6 +476,18 @@ describe('adagioAdapter', () => { expect(request.data.gdpr).to.exist; expect(request.data.gdpr.consentString).to.exist.and.to.equal(consentString); expect(request.data.gdpr.consentRequired).to.exist.and.to.equal(0); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(1); + }); + + it('GDPR consent is not applied w/ TCF2', () => { + bidderRequestTCF2.gdprConsent.gdprApplies = false; + const requests = spec.buildRequests(bidRequests, bidderRequestTCF2); + expect(requests).to.have.lengthOf(2); + const request = requests[0]; + expect(request.data.gdpr).to.exist; + expect(request.data.gdpr.consentString).to.exist.and.to.equal(consentString); + expect(request.data.gdpr.consentRequired).to.exist.and.to.equal(0); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(2); }); it('GDPR consent is undefined', () => { @@ -456,6 +501,20 @@ describe('adagioAdapter', () => { expect(request.data.gdpr).to.not.have.property('consentString'); expect(request.data.gdpr).to.not.have.property('gdprApplies'); expect(request.data.gdpr).to.not.have.property('allowAuctionWithoutConsent'); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(1); + }); + + it('GDPR consent is undefined w/ TCF2', () => { + delete bidderRequestTCF2.gdprConsent.consentString; + delete bidderRequestTCF2.gdprConsent.gdprApplies; + delete bidderRequestTCF2.gdprConsent.vendorData; + const requests = spec.buildRequests(bidRequests, bidderRequestTCF2); + expect(requests).to.have.lengthOf(2); + const request = requests[0]; + expect(request.data.gdpr).to.exist; + expect(request.data.gdpr).to.not.have.property('consentString'); + expect(request.data.gdpr).to.not.have.property('gdprApplies'); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(2); }); it('GDPR consent bidderRequest does not have gdprConsent', () => { @@ -480,6 +539,40 @@ describe('adagioAdapter', () => { }); expect(requests).to.be.empty; }); + + it('Should add the schain if available at bidder level', () => { + const bidRequest = Object.assign({}, bidRequests[0], { + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'ssp.test', + sid: '00001', + hp: 1 + }] + } + }); + + const requests = spec.buildRequests([bidRequest], bidderRequest); + const request = requests[0]; + + expect(request.data.schain).to.exist; + expect(request.data.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'ssp.test', + sid: '00001', + hp: 1 + }] + }); + }); + + it('Schain should not be added to the request', () => { + const requests = spec.buildRequests([bidRequests[0]], bidderRequest); + const request = requests[0]; + expect(request.data.schain).to.not.exist; + }); }); describe('interpretResponse', () => { From 681866445894a688e6c3714c55759b3209c9246b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Alberto=20Polo=20Garc=C3=ADa?= Date: Tue, 7 Apr 2020 15:02:07 +0200 Subject: [PATCH 64/90] Use TCFv2 consent data in Criteo Id module (#5073) Co-authored-by: Jesus Alberto Polo Garcia --- modules/criteoIdSystem.js | 17 ++++++++++++----- test/spec/modules/criteoIdSystem_spec.js | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index 0103db8ef80..d03e5de26c7 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -59,18 +59,19 @@ function getCriteoDataFromAllStorages() { } } -function buildCriteoUsersyncUrl(topUrl, domain, bundle, areCookiesWriteable, isPublishertagPresent) { +function buildCriteoUsersyncUrl(topUrl, domain, bundle, areCookiesWriteable, isPublishertagPresent, gdprString) { const url = 'https://gum.criteo.com/sid/json?origin=prebid' + `${topUrl ? '&topUrl=' + encodeURIComponent(topUrl) : ''}` + `${domain ? '&domain=' + encodeURIComponent(domain) : ''}` + `${bundle ? '&bundle=' + encodeURIComponent(bundle) : ''}` + + `${gdprString ? '&gdprString=' + encodeURIComponent(gdprString) : ''}` + `${areCookiesWriteable ? '&cw=1' : ''}` + `${isPublishertagPresent ? '&pbt=1' : ''}` return url; } -function callCriteoUserSync(parsedCriteoData) { +function callCriteoUserSync(parsedCriteoData, gdprString) { const cw = areCookiesWriteable(); const topUrl = extractProtocolHost(getRefererInfo().referer); const domain = extractProtocolHost(document.location.href, true); @@ -81,7 +82,8 @@ function callCriteoUserSync(parsedCriteoData) { domain, parsedCriteoData.bundle, cw, - isPublishertagPresent + isPublishertagPresent, + gdprString ); ajax.ajaxBuilder()( @@ -122,11 +124,16 @@ export const criteoIdSubmodule = { /** * get the Criteo Id from local storages and initiate a new user sync * @function + * @param {SubmoduleParams} [configParams] + * @param {ConsentData} [consentData] * @returns {{id: {criteoId: string} | undefined}}} */ - getId() { + getId(configParams, consentData) { + const hasGdprData = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies; + const gdprConsentString = hasGdprData ? consentData.consentString : undefined; + let localData = getCriteoDataFromAllStorages(); - callCriteoUserSync(localData); + callCriteoUserSync(localData, gdprConsentString); return { id: localData.bidId ? { criteoId: localData.bidId } : undefined } } diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index dee9a0563e2..74caef69ccc 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -132,4 +132,26 @@ describe('CriteoId module', function () { } }); })); + + const gdprConsentTestCases = [ + { consentData: { gdprApplies: true, consentString: 'expectedConsentString' }, expected: 'expectedConsentString' }, + { consentData: { gdprApplies: false, consentString: 'expectedConsentString' }, expected: undefined }, + { consentData: { gdprApplies: true, consentString: undefined }, expected: undefined }, + { consentData: { gdprApplies: 'oui', consentString: 'expectedConsentString' }, expected: undefined }, + { consentData: undefined, expected: undefined } + ]; + + gdprConsentTestCases.forEach(testCase => it('should call user sync url with the gdprConsent', function () { + const emptyObj = '{}'; + let ajaxStub = sinon.stub().callsFake((url, callback) => callback(emptyObj)); + ajaxBuilderStub.callsFake(mockResponse(undefined, ajaxStub)) + + criteoIdSubmodule.getId(undefined, testCase.consentData); + + if (testCase.expected) { + expect(ajaxStub.calledWithMatch(`gdprString=${testCase.expected}`)).to.be.true; + } else { + expect(ajaxStub.calledWithMatch('gdprString')).not.to.be.true; + } + })); }); From a7d44103451a5d8f7e708bd4135fbbe4eb68407e Mon Sep 17 00:00:00 2001 From: Jaimin Panchal <7393273+jaiminpanchal27@users.noreply.github.com> Date: Tue, 7 Apr 2020 13:07:49 -0400 Subject: [PATCH 65/90] Update param in request payload (#5091) Co-authored-by: Jaimin Panchal --- modules/appnexusBidAdapter.js | 5 ++++- test/spec/modules/appnexusBidAdapter_spec.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 9c98ccc0651..db621217025 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -74,7 +74,10 @@ export const spec = { if (userObjBid) { Object.keys(userObjBid.params.user) .filter(param => includes(USER_PARAMS, param)) - .forEach(param => userObj[param] = userObjBid.params.user[param]); + .forEach((param) => { + let uparam = utils.convertCamelToUnderscore(param); + userObj[uparam] = userObjBid.params.user[param] + }); } const appDeviceObjBid = find(bidRequests, hasAppDeviceInfo); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 2a6a25752d9..8934d1cdaef 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -258,7 +258,7 @@ describe('AppNexusAdapter', function () { expect(payload.user).to.exist; expect(payload.user).to.deep.equal({ - externalUid: '123', + external_uid: '123', }); }); From dc02ff460bf063766097a312d7c1ae17f40df5e5 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Tue, 7 Apr 2020 10:34:05 -0700 Subject: [PATCH 66/90] PubMatic bid adapter to support app object set using setConfig (#5090) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * merge the device object from setConfig * set app from config and site, remove site if app is set * added test cases for reading device from config * unit test case for reading app from conf * delete the site object * set undefined not null --- modules/pubmaticBidAdapter.js | 15 +++++ test/spec/modules/pubmaticBidAdapter_spec.js | 64 ++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 49f193cf93e..2c1024b6fe3 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -920,6 +920,11 @@ export const spec = { payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim(); payload.site.domain = _getDomainFromURL(payload.site.page); + // merge the device from config.getConfig('device') + if (typeof config.getConfig('device') === 'object') { + payload.device = Object.assign(payload.device, config.getConfig('device')); + } + // passing transactionId in source.tid utils.deepSetValue(payload, 'source.tid', conf.transactionId); @@ -953,6 +958,16 @@ export const spec = { _handleEids(payload, validBidRequests); _blockedIabCategoriesValidation(payload, blockedIabCategories); + // Note: Do not move this block up + // if site object is set in Prebid config then we need to copy required fields from site into app and unset the site object + if (typeof config.getConfig('app') === 'object') { + payload.app = config.getConfig('app'); + // not copying domain from site as it is a derived value from page + payload.app.publisher = payload.site.publisher; + payload.app.ext = payload.site.ext || UNDEFINED; + delete payload.site; + } + return { method: 'POST', url: ENDPOINT, diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 36b984296fc..533edb33858 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -769,6 +769,70 @@ describe('PubMatic adapter', function () { expect(data.source.ext.schain).to.deep.equal(bidRequests[0].schain); }); + it('Merge the device info from config', function() { + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + device: { + 'newkey': 'new-device-data' + } + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.device.js).to.equal(1); + expect(data.device.dnt).to.equal((navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0); + expect(data.device.h).to.equal(screen.height); + expect(data.device.w).to.equal(screen.width); + expect(data.device.language).to.equal(navigator.language); + expect(data.device.newkey).to.equal('new-device-data');// additional data from config + sandbox.restore(); + }); + + it('Merge the device info from config; data from config overrides the info we have gathered', function() { + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + device: { + newkey: 'new-device-data', + language: 'MARATHI' + } + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.device.js).to.equal(1); + expect(data.device.dnt).to.equal((navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0); + expect(data.device.h).to.equal(screen.height); + expect(data.device.w).to.equal(screen.width); + expect(data.device.language).to.equal('MARATHI');// // data overriding from config + expect(data.device.newkey).to.equal('new-device-data');// additional data from config + sandbox.restore(); + }); + + it('Set app from config, copy publisher and ext from site, unset site', function() { + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + app: { + bundle: 'org.prebid.mobile.demoapp', + domain: 'prebid.org' + } + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); + expect(data.app.domain).to.equal('prebid.org'); + expect(data.app.publisher.id).to.equal(bidRequests[0].params.publisherId); + expect(data.app.ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); + expect(data.site).to.not.exist; + sandbox.restore(); + }); + it('Request params check: without adSlot', function () { delete bidRequests[0].params.adSlot; let request = spec.buildRequests(bidRequests); From 977f546deed1e69cddcc805d16c3a89f363a113a Mon Sep 17 00:00:00 2001 From: Vedant Seta Date: Tue, 7 Apr 2020 23:07:36 +0530 Subject: [PATCH 67/90] Media.net : fetches gdpr consent string (#5058) Co-authored-by: vedant.s --- modules/medianetAnalyticsAdapter.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js index 63f2e2329a2..35a050831f3 100644 --- a/modules/medianetAnalyticsAdapter.js +++ b/modules/medianetAnalyticsAdapter.js @@ -346,11 +346,15 @@ function auctionInitHandler({auctionId, timestamp}) { } } -function bidRequestedHandler({ auctionId, auctionStart, bids, start, timeout, uspConsent, gdprConsent }) { +function bidRequestedHandler({ auctionId, auctionStart, bids, start, timeout, uspConsent, gdpr }) { if (!(auctions[auctionId] instanceof Auction)) { return; } - config.gdprConsent = config.gdprConsent || gdprConsent; + + if (gdpr && gdpr.gdprApplies) { + config.gdprConsent = gdpr.consentString || ''; + } + config.uspConsent = config.uspConsent || uspConsent; bids.forEach(bid => { @@ -542,7 +546,15 @@ function fireAuctionLog(acid, adtag, isBidWonEvent) { } function formatQS(data) { - return utils._map(data, (value, key) => value === undefined ? key + '=' : key + '=' + encodeURIComponent(value)).join('&'); + return utils._map(data, (value, key) => { + if (value === undefined) { + return key + '='; + } + if (utils.isPlainObject(value)) { + value = JSON.stringify(value); + } + return key + '=' + encodeURIComponent(value); + }).join('&'); } function firePixel(qs) { From 9de6609380a263c6794fe973da05fa77be7ca273 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen <14229985+Fawke@users.noreply.github.com> Date: Tue, 7 Apr 2020 23:22:20 +0530 Subject: [PATCH 68/90] Size Mapping V2 supports 'Identical Ad Units' (#5062) * add support for identical ad units and improve log messages * slight change in log message presentation * fix typo * document return value type --- allowedModules.js | 15 +-- modules/sizeMappingV2.js | 41 ++++---- src/utils.js | 14 ++- test/spec/modules/sizeMappingV2_spec.js | 127 ++++++++++++++++++++---- 4 files changed, 152 insertions(+), 45 deletions(-) diff --git a/allowedModules.js b/allowedModules.js index 0fee2cfdedd..4f8b8039d97 100644 --- a/allowedModules.js +++ b/allowedModules.js @@ -1,11 +1,11 @@ const sharedWhiteList = [ - "core-js/library/fn/array/find", // no ie11 - "core-js/library/fn/array/includes", // no ie11 - "core-js/library/fn/set", // ie11 supports Set but not Set#values - "core-js/library/fn/string/includes", // no ie11 - "core-js/library/fn/number/is-integer", // no ie11, - "core-js/library/fn/array/from" // no ie11 + 'core-js/library/fn/array/find', // no ie11 + 'core-js/library/fn/array/includes', // no ie11 + 'core-js/library/fn/set', // ie11 supports Set but not Set#values + 'core-js/library/fn/string/includes', // no ie11 + 'core-js/library/fn/number/is-integer', // no ie11, + 'core-js/library/fn/array/from' // no ie11 ]; module.exports = { @@ -21,6 +21,7 @@ module.exports = { 'fun-hooks/no-eval', 'just-clone', 'dlv', - 'dset' + 'dset', + 'deep-equal' ] }; diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index ec3604faa97..98effb301fb 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -273,21 +273,21 @@ export function isLabelActivated(bidOrAdUnit, activeLabels, adUnitCode) { let labelOperator; const labelsFound = Object.keys(bidOrAdUnit).filter(prop => prop === 'labelAny' || prop === 'labelAll'); if (labelsFound && labelsFound.length > 1) { - utils.logWarn(`SizeMappingV2:: ${(bidOrAdUnit.code) - ? (`Ad Unit: ${bidOrAdUnit.code} has multiple label operators. Using the first declared operator: ${labelsFound[0]}`) - : (`Bidder: ${bidOrAdUnit.bidder} in Ad Unit: ${adUnitCode} has multiple label operators. Using the first declared operator: ${labelsFound[0]}`)}`); + utils.logWarn(`Size Mapping V2:: ${(bidOrAdUnit.code) + ? (`Ad Unit: ${bidOrAdUnit.code} => Ad unit has multiple label operators. Using the first declared operator: ${labelsFound[0]}`) + : (`Ad Unit: ${adUnitCode}, Bidder: ${bidOrAdUnit.bidder} => Bidder has multiple label operators. Using the first declared operator: ${labelsFound[0]}`)}`); } labelOperator = labelsFound[0]; if (labelOperator === 'labelAll' && Array.isArray(bidOrAdUnit[labelOperator])) { if (bidOrAdUnit.labelAll.length === 0) { - utils.logWarn(`SizeMappingV2:: Ad Unit: ${bidOrAdUnit.code} has declared property labelAll with an empty array. Ad Unit is still enabled!`); + utils.logWarn(`Size Mapping V2:: Ad Unit: ${bidOrAdUnit.code} => Ad unit has declared property 'labelAll' with an empty array. Ad Unit is still enabled!`); return true; } return bidOrAdUnit.labelAll.every(label => includes(activeLabels, label)); } else if (labelOperator === 'labelAny' && Array.isArray(bidOrAdUnit[labelOperator])) { if (bidOrAdUnit.labelAny.length === 0) { - utils.logWarn(`SizeMappingV2:: Ad Unit: ${bidOrAdUnit.code} has declared property labelAny with an empty array. Ad Unit is still enabled!`); + utils.logWarn(`Size Mapping V2:: Ad Unit: ${bidOrAdUnit.code} => Ad unit has declared property 'labelAny' with an empty array. Ad Unit is still enabled!`); return true; } return bidOrAdUnit.labelAny.some(label => includes(activeLabels, label)); @@ -430,7 +430,11 @@ export function getRelevantMediaTypesForBidder(sizeConfig, activeViewport) { // sets sizeMappingInternalStore for a given auctionId with relevant adUnit information returned from the call to 'getFilteredMediaTypes' function // returns adUnit details object. export function getAdUnitDetail(auctionId, adUnit) { - const adUnitDetail = sizeMappingInternalStore.getAuctionDetail(auctionId).adUnits.filter(adUnitDetail => adUnitDetail.adUnitCode === adUnit.code); + // fetch all adUnits for an auction from the sizeMappingInternalStore + const adUnitsForAuction = sizeMappingInternalStore.getAuctionDetail(auctionId).adUnits; + + // check if the adUnit exists already in the sizeMappingInterStore (check for equivalence of 'code' && 'mediaTypes' properties) + const adUnitDetail = adUnitsForAuction.filter(adUnitDetail => adUnitDetail.adUnitCode === adUnit.code && utils.deepEqual(adUnitDetail.mediaTypes, adUnit.mediaTypes)); if (adUnitDetail.length > 0) { return adUnitDetail[0]; @@ -445,16 +449,17 @@ export function getAdUnitDetail(auctionId, adUnit) { transformedMediaTypes }; + // set adUnitDetail in sizeMappingInternalStore against the correct 'auctionId'. + sizeMappingInternalStore.setAuctionDetail(auctionId, adUnitDetail); + // 'filteredMediaTypes' are the mediaTypes that got removed/filtered-out from adUnit.mediaTypes after sizeConfig filtration. - const filteredMediaTypes = Object.keys(mediaTypes).filter(mt => Object.keys(transformedMediaTypes).indexOf(mt) === -1) + const filteredMediaTypes = Object.keys(mediaTypes).filter(mt => Object.keys(transformedMediaTypes).indexOf(mt) === -1); - utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code} - Active size buckets after filtration: `, sizeBucketToSizeMap); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Active size buckets after filtration: `, sizeBucketToSizeMap); if (filteredMediaTypes.length > 0) { - utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code} - mediaTypes that got filtered out: ${filteredMediaTypes}`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Media types that got filtered out: ${filteredMediaTypes}`); } - // set adUnitDetail in sizeMappingInternalStore against the correct 'auctionId'. - sizeMappingInternalStore.setAuctionDetail(auctionId, adUnitDetail); return adUnitDetail; } } @@ -468,7 +473,7 @@ export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, label // check if adUnit has any active media types remaining, if not drop the adUnit from auction, // else proceed to evaluate the bids object. if (Object.keys(transformedMediaTypes).length === 0) { - utils.logInfo(`SizeMappingV2:: Ad Unit: ${adUnit.code} is disabled since there are no active media types after sizeConfig filtration.`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Ad unit disabled since there are no active media types after sizeConfig filtration.`); return result; } result @@ -488,7 +493,7 @@ export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, label if (bid.sizeConfig) { const relevantMediaTypes = internal.getRelevantMediaTypesForBidder(bid.sizeConfig, activeViewport); if (relevantMediaTypes.length === 0) { - utils.logError(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bidderCode} - sizeConfig is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); + utils.logError(`Size Mapping V2:: Ad Unit: ${adUnit.code}, Bidder: ${bidderCode} => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); bid = Object.assign({}, bid); } else if (relevantMediaTypes[0] !== 'none') { const bidderMediaTypes = Object @@ -502,11 +507,11 @@ export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, label if (Object.keys(bidderMediaTypes).length > 0) { bid = Object.assign({}, bid, { mediaTypes: bidderMediaTypes }); } else { - utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bid.bidder} - 'relevantMediaTypes' for this bidder does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}, Bidder: ${bid.bidder} => 'relevantMediaTypes' does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); return bids; } } else { - utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bid.bidder} - 'relevantMediaTypes' is set to 'none' in sizeConfig for current viewport size. This bidder is disabled.`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}, Bidder: ${bid.bidder} => 'relevantMediaTypes' is set to 'none' in sizeConfig for current viewport size. This bidder is disabled.`); return bids; } } @@ -525,15 +530,15 @@ export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, label })); return bids; } else { - utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bid.bidder} - Label check for this bidder has failed. This bidder is disabled.`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}, Bidder: ${bid.bidder} => Label check for this bidder has failed. This bidder is disabled.`); return bids; } }, [])); } else { - utils.logWarn(`SizeMappingV2:: Ad Unit: ${adUnit.code} has declared invalid mediaTypes or has not declared a mediaTypes property`); + utils.logWarn(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Ad unit has declared invalid 'mediaTypes' or has not declared a 'mediaTypes' property`); } } else { - utils.logInfo(`SizeMappingV2:: Ad Unit: ${adUnit.code} is disabled due to failing label check.`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Ad unit is disabled due to failing label check.`); return result; } return result; diff --git a/src/utils.js b/src/utils.js index d9c4e5ea0f8..f43097e74b9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import { config } from './config.js'; import clone from 'just-clone'; +import deepequal from 'deep-equal'; import find from 'core-js/library/fn/array/find.js'; import includes from 'core-js/library/fn/array/includes.js'; @@ -36,7 +37,8 @@ export const internal = { logError, logWarn, logMessage, - logInfo + logInfo, + deepEqual }; var uniqueRef = {}; @@ -1242,3 +1244,13 @@ export function compareOn(property) { return 0; } } + +/** + * This function compares two objects for checking their equivalence. + * @param {Object} obj1 + * @param {Object} obj2 + * @returns {boolean} + */ +export function deepEqual(obj1, obj2) { + return deepequal(obj1, obj2); +} diff --git a/test/spec/modules/sizeMappingV2_spec.js b/test/spec/modules/sizeMappingV2_spec.js index 4b76ce2fa1c..cbfc02752a8 100644 --- a/test/spec/modules/sizeMappingV2_spec.js +++ b/test/spec/modules/sizeMappingV2_spec.js @@ -760,7 +760,7 @@ describe('sizeMappingV2', function () { isLabelActivated(adUnits, activeLabels, adUnitCode); sinon.assert.callCount(utils.logWarn, 1); - sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: div-gpt-ad-1460505748561-0 has multiple label operators. Using the first declared operator: labelAny`); + sinon.assert.calledWith(utils.logWarn, `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-0 => Ad unit has multiple label operators. Using the first declared operator: labelAny`); }); it('should throw a warning message if both the label operator, "labelAny"/"labelAll" are configured for an Bidder', function () { @@ -772,7 +772,7 @@ describe('sizeMappingV2', function () { isLabelActivated(adUnits.bids[0], activeLabels, adUnitCode); sinon.assert.callCount(utils.logWarn, 1); - sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Bidder: appnexus in Ad Unit: div-gpt-ad-1460505748561-0 has multiple label operators. Using the first declared operator: labelAny`); + sinon.assert.calledWith(utils.logWarn, `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-0, Bidder: appnexus => Bidder has multiple label operators. Using the first declared operator: labelAny`); }); it('should give priority to the label operator declared first incase two label operators are found on the same Ad Unit or Bidder', function () { @@ -805,13 +805,13 @@ describe('sizeMappingV2', function () { isLabelActivated(adUnit, activeLabels, adUnitCode); sinon.assert.callCount(utils.logWarn, 1); - sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: div-gpt-ad-1460505748561-0 has declared property labelAll with an empty array. Ad Unit is still enabled!`); + sinon.assert.calledWith(utils.logWarn, `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-0 => Ad unit has declared property 'labelAll' with an empty array. Ad Unit is still enabled!`); // bidder level check isLabelActivated(adUnit.bids[0], activeLabels, adUnitCode); sinon.assert.callCount(utils.logWarn, 1); - sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: div-gpt-ad-1460505748561-0 has declared property labelAll with an empty array. Ad Unit is still enabled!`); + sinon.assert.calledWith(utils.logWarn, `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-0 => Ad unit has declared property 'labelAll' with an empty array. Ad Unit is still enabled!`); }); it('should throw a warning log message if "labelAny" operator is declared as an empty array', function () { @@ -822,12 +822,12 @@ describe('sizeMappingV2', function () { isLabelActivated(adUnit, activeLabels, adUnitCode); sinon.assert.callCount(utils.logWarn, 1); - sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: div-gpt-ad-1460505748561-0 has declared property labelAny with an empty array. Ad Unit is still enabled!`); + sinon.assert.calledWith(utils.logWarn, `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-0 => Ad unit has declared property 'labelAny' with an empty array. Ad Unit is still enabled!`); // bidder level check isLabelActivated(adUnit.bids[0], activeLabels, adUnitCode); sinon.assert.callCount(utils.logWarn, 1); - sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: div-gpt-ad-1460505748561-0 has declared property labelAny with an empty array. Ad Unit is still enabled!`); + sinon.assert.calledWith(utils.logWarn, `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-0 => Ad unit has declared property 'labelAny' with an empty array. Ad Unit is still enabled!`); }); it('should return "true" if label operators are not present on the Ad Unit or Bidder', function () { @@ -1001,7 +1001,55 @@ describe('sizeMappingV2', function () { describe('getAdUnitDetail(auctionId, adUnit)', function () { const adUnitDetailFixture_1 = { adUnitCode: 'div-gpt-ad-1460505748561-0', - mediaTypes: {}, + mediaTypes: { + banner: { + sizeConfig: [ + { minViewPort: [0, 0], sizes: [] }, // remove if < 750px + { minViewPort: [750, 0], sizes: [[300, 250], [300, 600]] }, // between 750px and 1199px + { minViewPort: [1200, 0], sizes: [[970, 90], [728, 90], [300, 250]] }, // between 1200px and 1599px + { minViewPort: [1600, 0], sizes: [[1000, 300], [970, 90], [728, 90], [300, 250]] } // greater than 1600px + ] + }, + video: { + context: 'instream', + sizeConfig: [ + { minViewPort: [0, 0], playerSize: [] }, + { minViewPort: [800, 0], playerSize: [[640, 400]] }, + { minViewPort: [1200, 0], playerSize: [] } + ] + }, + native: { + image: { + required: true, + sizes: [150, 50] + }, + title: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + privacyLink: { + required: false + }, + body: { + required: true + }, + icon: { + required: true, + sizes: [50, 50] + }, + sizeConfig: [ + { minViewPort: [0, 0], active: false }, + { minViewPort: [600, 0], active: true }, + { minViewPort: [1000, 0], active: false } + ] + } + }, sizeBucketToSizeMap: {}, activeViewport: {}, transformedMediaTypes: {} @@ -1021,6 +1069,21 @@ describe('sizeMappingV2', function () { activeViewport: {}, transformedMediaTypes: { banner: {}, video: {} } } + // adunit with same code at adUnitDetailFixture_1 but differnet mediaTypes object + const adUnitDetailFixture_3 = { + adUnitCode: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizeConfig: [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [1000, 0], sizes: [[1000, 300], [1000, 90], [970, 250], [970, 90], [728, 90]] } + ] + } + }, + sizeBucketToSizeMap: {}, + activeViewport: {}, + transformedMediaTypes: {} + } beforeEach(function () { sinon .stub(sizeMappingInternalStore, 'getAuctionDetail') @@ -1034,12 +1097,18 @@ describe('sizeMappingV2', function () { .stub(sizeMappingInternalStore, 'setAuctionDetail') .withArgs('a1b2c3', adUnitDetailFixture_2); - sinon - .stub(internal, 'getFilteredMediaTypes') - .withArgs(adUnitDetailFixture_2.mediaTypes) + const getFilteredMediaTypesStub = sinon.stub(internal, 'getFilteredMediaTypes'); + + getFilteredMediaTypesStub + .withArgs(AD_UNITS[1].mediaTypes) .returns(adUnitDetailFixture_2); + getFilteredMediaTypesStub + .withArgs(adUnitDetailFixture_3.mediaTypes) + .returns(adUnitDetailFixture_3); + sinon.spy(utils, 'logInfo'); + sinon.spy(utils, 'deepEqual'); }); afterEach(function () { @@ -1047,15 +1116,35 @@ describe('sizeMappingV2', function () { sizeMappingInternalStore.setAuctionDetail.restore(); internal.getFilteredMediaTypes.restore(); utils.logInfo.restore(); + utils.deepEqual.restore(); }); - it('should return adUnit detail object from "sizeMappingInternalStore" if adUnit is alreay present in the store', function () { + it('should return adUnit detail object from "sizeMappingInternalStore" if adUnit is already present in the store', function () { const [adUnit] = utils.deepClone(AD_UNITS); const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit); sinon.assert.callCount(sizeMappingInternalStore.getAuctionDetail, 1); + sinon.assert.callCount(utils.deepEqual, 1); + sinon.assert.callCount(internal.getFilteredMediaTypes, 0); expect(adUnitDetail).to.deep.equal(adUnitDetailFixture_1); }); + it('should NOT return adunit detail object from "sizeMappingInternalStore" if adUnit with the SAME CODE BUT DIFFERENT MEDIATYPES OBJECT is present in the store', function() { + const [adUnit] = utils.deepClone(AD_UNITS); + adUnit.mediaTypes = { + banner: { + sizeConfig: [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [1000, 0], sizes: [[1000, 300], [1000, 90], [970, 250], [970, 90], [728, 90]] } + ] + } + }; + const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit); + sinon.assert.callCount(sizeMappingInternalStore.getAuctionDetail, 1); + sinon.assert.callCount(utils.deepEqual, 1); + expect(adUnitDetail).to.not.deep.equal(adUnitDetailFixture_1); + sinon.assert.callCount(internal.getFilteredMediaTypes, 1); + }); + it('should store value in "sizeMappingInterStore" object if adUnit is NOT preset in this object', function () { const [, adUnit] = utils.deepClone(AD_UNITS); const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit); @@ -1068,7 +1157,7 @@ describe('sizeMappingV2', function () { const [, adUnit] = utils.deepClone(AD_UNITS); getAdUnitDetail('a1b2c3', adUnit); sinon.assert.callCount(utils.logInfo, 1); - sinon.assert.calledWith(utils.logInfo, `SizeMappingV2:: AdUnit: div-gpt-ad-1460505748561-1 - Active size buckets after filtration: `, adUnitDetailFixture_2.sizeBucketToSizeMap); + sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-1 => Active size buckets after filtration: `, adUnitDetailFixture_2.sizeBucketToSizeMap); }); it('should log info message if any of the mediaTypes defined in adUnit.mediaTypes got filtered out', function () { @@ -1100,7 +1189,7 @@ describe('sizeMappingV2', function () { getAdUnitDetail('a1b2c3', adUnit); sinon.assert.callCount(utils.logInfo, 2); - sinon.assert.calledWith(utils.logInfo.getCall(1), `SizeMappingV2:: AdUnit: div-gpt-ad-1460505748561-1 - mediaTypes that got filtered out: video`); + sinon.assert.calledWith(utils.logInfo.getCall(1), `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-1 => Media types that got filtered out: video`); }); }); @@ -1385,7 +1474,7 @@ describe('sizeMappingV2', function () { }); expect(bidRequests[0]).to.be.undefined; sinon.assert.callCount(utils.logInfo, 1); - sinon.assert.calledWith(utils.logInfo, `SizeMappingV2:: Ad Unit: adUnit1 is disabled since there are no active media types after sizeConfig filtration.`); + sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1 => Ad unit disabled since there are no active media types after sizeConfig filtration.`); }); it('should throw an error if bidder level sizeConfig is not configured properly', function () { @@ -1413,7 +1502,7 @@ describe('sizeMappingV2', function () { expect(bidRequests[0]).to.not.be.undefined; sinon.assert.callCount(utils.logError, 1); - sinon.assert.calledWith(utils.logError, `SizeMappingV2:: AdUnit: adUnit1, Bidder: rubicon - sizeConfig is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); + sinon.assert.calledWith(utils.logError, `Size Mapping V2:: Ad Unit: adUnit1, Bidder: rubicon => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); }); it('should ensure bidder relevantMediaTypes is a subset of active media types at the ad unit level', function () { @@ -1514,7 +1603,7 @@ describe('sizeMappingV2', function () { }); expect(bidRequests[0]).to.be.undefined; sinon.assert.callCount(utils.logInfo, 1); - sinon.assert.calledWith(utils.logInfo, `SizeMappingV2:: AdUnit: adUnit1, Bidder: rubicon - 'relevantMediaTypes' for this bidder does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); + sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1, Bidder: rubicon => 'relevantMediaTypes' does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); }); it('should throw a warning if mediaTypes object is not correctly formatted', function () { @@ -1531,7 +1620,7 @@ describe('sizeMappingV2', function () { src: 'client' }); sinon.assert.callCount(utils.logWarn, 1); - sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: adUnit1 has declared invalid mediaTypes or has not declared a mediaTypes property`); + sinon.assert.calledWith(utils.logWarn, `Size Mapping V2:: Ad Unit: adUnit1 => Ad unit has declared invalid 'mediaTypes' or has not declared a 'mediaTypes' property`); utils.isValidMediaTypes.restore(); }); @@ -1552,7 +1641,7 @@ describe('sizeMappingV2', function () { }); sinon.assert.callCount(utils.logInfo, 1); - sinon.assert.calledWith(utils.logInfo, `SizeMappingV2:: Ad Unit: adUnit1 is disabled due to failing label check.`); + sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1 => Ad unit is disabled due to failing label check.`); internal.isLabelActivated.restore(); }); @@ -1572,7 +1661,7 @@ describe('sizeMappingV2', function () { }); sinon.assert.callCount(utils.logInfo, 1); - sinon.assert.calledWith(utils.logInfo, `SizeMappingV2:: AdUnit: adUnit1, Bidder: appnexus - Label check for this bidder has failed. This bidder is disabled.`); + sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1, Bidder: appnexus => Label check for this bidder has failed. This bidder is disabled.`); internal.isLabelActivated.restore(); }) From 73d5861ae9f631168bd0dcb2688e1abbcb782c1b Mon Sep 17 00:00:00 2001 From: Maxime Lequain Date: Tue, 7 Apr 2020 20:19:27 +0200 Subject: [PATCH 69/90] add adot bidder adapter (#4949) * add adot bidder adapter * remove promises and other methods to ensure IE11 compatibility and remove duplicate code Co-authored-by: Maxime Lequain --- modules/adotBidAdapter.js | 607 +++++ modules/adotBidAdapter.md | 218 ++ test/spec/modules/adotBidAdapter_spec.js | 3118 ++++++++++++++++++++++ 3 files changed, 3943 insertions(+) create mode 100644 modules/adotBidAdapter.js create mode 100644 modules/adotBidAdapter.md create mode 100644 test/spec/modules/adotBidAdapter_spec.js diff --git a/modules/adotBidAdapter.js b/modules/adotBidAdapter.js new file mode 100644 index 00000000000..94672b20c0c --- /dev/null +++ b/modules/adotBidAdapter.js @@ -0,0 +1,607 @@ +import {Renderer} from '../src/Renderer.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {isStr, isArray, isNumber, isPlainObject, isBoolean, logError} from '../src/utils.js'; +import find from 'core-js/library/fn/array/find.js'; + +const ADAPTER_VERSION = 'v1.0.0'; +const BID_METHOD = 'POST'; +const BIDDER_URL = 'https://dsp.adotmob.com/headerbidding/bidrequest'; +const SUPPORTED_VIDEO_CONTEXTS = ['instream', 'outstream']; +const SUPPORTED_INSTREAM_CONTEXTS = ['pre-roll', 'mid-roll', 'post-roll']; +const NATIVE_PLACEMENTS = { + title: {id: 1, name: 'title'}, + icon: {id: 2, type: 1, name: 'img'}, + image: {id: 3, type: 3, name: 'img'}, + sponsoredBy: {id: 4, name: 'data', type: 1}, + body: {id: 5, name: 'data', type: 2}, + cta: {id: 6, type: 12, name: 'data'} +}; +const NATIVE_ID_MAPPING = {1: 'title', 2: 'icon', 3: 'image', 4: 'sponsoredBy', 5: 'body', 6: 'cta'}; +const SUPPORTED_VIDEO_MIMES = ['video/mp4']; +const DOMAIN_REGEX = new RegExp('//([^/]*)'); +const FIRST_PRICE = 1; +const BID_SUPPORTED_MEDIA_TYPES = ['banner', 'video', 'native']; +const TTL = 10; +const NET_REVENUE = true; +// eslint-disable-next-line no-template-curly-in-string +const AUCTION_PRICE = '${AUCTION_PRICE}'; +const OUTSTREAM_VIDEO_PLAYER_URL = 'https://adserver.adotmob.com/video/player.min.js'; + +function isNone(value) { + return (value === null) || (value === undefined); +} + +function groupBy(values, key) { + const groups = values.reduce((acc, value) => { + const groupId = value[key]; + + if (!acc[groupId]) acc[groupId] = []; + acc[groupId].push(value); + + return acc; + }, {}); + + return Object + .keys(groups) + .map(id => ({id, key, values: groups[id]})); +} + +function validateMediaTypes(mediaTypes, allowedMediaTypes) { + if (!isPlainObject(mediaTypes)) return false; + if (!allowedMediaTypes.some(mediaType => mediaType in mediaTypes)) return false; + + if (isBanner(mediaTypes)) { + if (!validateBanner(mediaTypes.banner)) return false; + } + + if (isVideo(mediaTypes)) { + if (!validateVideo(mediaTypes.video)) return false; + } + + return true; +} + +function isBanner(mediaTypes) { + return isPlainObject(mediaTypes) && isPlainObject(mediaTypes.banner); +} + +function isVideo(mediaTypes) { + return isPlainObject(mediaTypes) && isPlainObject(mediaTypes.video); +} + +function validateBanner(banner) { + return isPlainObject(banner) && + isArray(banner.sizes) && + (banner.sizes.length > 0) && + banner.sizes.every(validateMediaSizes); +} + +function validateVideo(video) { + if (!isPlainObject(video)) return false; + if (!isStr(video.context)) return false; + if (SUPPORTED_VIDEO_CONTEXTS.indexOf(video.context) === -1) return false; + + if (!video.playerSize) return true; + if (!isArray(video.playerSize)) return false; + + return video.playerSize.every(validateMediaSizes); +} + +function validateMediaSizes(mediaSize) { + return isArray(mediaSize) && + (mediaSize.length === 2) && + mediaSize.every(size => (isNumber(size) && size >= 0)); +} + +function validateParameters(parameters, adUnit) { + if (isVideo(adUnit.mediaTypes)) { + if (!isPlainObject(parameters)) return false; + if (!validateVideoParameters(parameters.video, adUnit)) return false; + } + + return true; +} + +function validateVideoParameters(video, adUnit) { + if (!video) return false; + + if (!isArray(video.mimes)) return false; + if (video.mimes.length === 0) return false; + if (!video.mimes.every(isStr)) return false; + + if (video.minDuration && !isNumber(video.minDuration)) return false; + if (video.maxDuration && !isNumber(video.maxDuration)) return false; + + if (!isArray(video.protocols)) return false; + if (video.protocols.length === 0) return false; + if (!video.protocols.every(isNumber)) return false; + + if (isInstream(adUnit.mediaTypes.video)) { + if (!video.instreamContext) return false; + if (SUPPORTED_INSTREAM_CONTEXTS.indexOf(video.instreamContext) === -1) return false; + } + + return true; +} + +function validateServerRequest(serverRequest) { + return isPlainObject(serverRequest) && + isPlainObject(serverRequest.data) && + isArray(serverRequest.data.imp) && + isPlainObject(serverRequest._adot_internal) && + isArray(serverRequest._adot_internal.impressions) +} + +function createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) { + return { + method: BID_METHOD, + url: BIDDER_URL, + data: generateBidRequestsFromAdUnits(adUnits, bidRequestId, adUnitContext), + _adot_internal: generateAdotInternal(adUnits) + } +} + +function generateAdotInternal(adUnits) { + const impressions = adUnits.reduce((acc, adUnit) => { + const {bidId, mediaTypes, adUnitCode, params} = adUnit; + const base = {bidId, adUnitCode, container: params.video && params.video.container}; + + const imps = Object + .keys(mediaTypes) + .reduce((acc, mediaType, index) => { + const data = mediaTypes[mediaType]; + const impressionId = `${bidId}_${index}`; + + if (mediaType !== 'banner') return acc.concat({...base, impressionId}); + + const bannerImps = data.sizes.map((item, i) => ({...base, impressionId: `${impressionId}_${i}`})); + + return acc.concat(bannerImps); + }, []); + + return acc.concat(imps); + }, []); + + return {impressions}; +} + +function generateBidRequestsFromAdUnits(adUnits, bidRequestId, adUnitContext) { + return { + id: bidRequestId, + imp: adUnits.reduce(generateImpressionsFromAdUnit, []), + site: generateSiteFromAdUnitContext(adUnitContext), + device: getDeviceInfo(), + user: getUserInfoFromAdUnitContext(adUnitContext), + regs: getRegulationFromAdUnitContext(adUnitContext), + at: FIRST_PRICE, + ext: generateBidRequestExtension() + }; +} + +function generateImpressionsFromAdUnit(acc, adUnit) { + const {bidId, mediaTypes, params} = adUnit; + const {placementId} = params; + const pmp = {}; + + if (placementId) pmp.deals = [{id: placementId}] + + const imps = Object + .keys(mediaTypes) + .reduce((acc, mediaType, index) => { + const data = mediaTypes[mediaType]; + const impId = `${bidId}_${index}`; + + if (mediaType === 'banner') return acc.concat(generateBannerFromAdUnit(impId, data, params)); + if (mediaType === 'video') return acc.concat({id: impId, video: generateVideoFromAdUnit(data, params), pmp}); + if (mediaType === 'native') return acc.concat({id: impId, native: generateNativeFromAdUnit(data, params), pmp}); + }, []); + + return acc.concat(imps); +} + +function isImpressionAVideo(impression) { + return isPlainObject(impression) && isPlainObject(impression.video); +} + +function generateBannerFromAdUnit(impId, data, params) { + const {position, placementId} = params; + const pos = position || 0; + const pmp = {}; + + if (placementId) pmp.deals = [{id: placementId}] + + return data.sizes.map(([w, h], index) => ({id: `${impId}_${index}`, banner: {format: [{w, h}], w, h, pos}, pmp})); +} + +function generateVideoFromAdUnit(data, params) { + const {playerSize} = data; + const hasPlayerSize = isArray(playerSize) && playerSize.length > 0; + const {position, video = {}} = params; + const {minDuration, maxDuration, protocols} = video; + + const size = {width: hasPlayerSize ? playerSize[0][0] : null, height: hasPlayerSize ? playerSize[0][1] : null}; + const duration = {min: isNumber(minDuration) ? minDuration : null, max: isNumber(maxDuration) ? maxDuration : null}; + + return { + mimes: SUPPORTED_VIDEO_MIMES, + w: size.width, + h: size.height, + startdelay: computeStartDelay(data, params), + minduration: duration.min, + maxduration: duration.max, + protocols, + pos: position || 0 + }; +} + +function isInstream(video) { + return isPlainObject(video) && (video.context === 'instream'); +} + +function isOutstream(video) { + return isPlainObject(video) && (video.startdelay === null) +} + +function computeStartDelay(data, params) { + if (isInstream(data)) { + if (params.video.instreamContext === 'pre-roll') return 0; + if (params.video.instreamContext === 'mid-roll') return -1; + if (params.video.instreamContext === 'post-roll') return -2; + } + + return null; +} + +function generateNativeFromAdUnit(data, params) { + const placements = NATIVE_PLACEMENTS; + const assets = Object + .keys(data) + .reduce((acc, placement) => { + const placementData = data[placement]; + const assetInfo = placements[placement]; + + if (!assetInfo) return acc; + + const {id, name, type} = assetInfo; + const {required, len, sizes} = placementData; + const wmin = sizes && sizes[0]; + const hmin = sizes && sizes[1]; + const content = {}; + + if (type) content.type = type; + if (len) content.len = len; + if (wmin) content.wmin = wmin; + if (hmin) content.hmin = hmin; + + acc.push({id, required, [name]: content}); + + return acc; + }, []); + + return { + request: JSON.stringify({assets}) + }; +} + +function generateSiteFromAdUnitContext(adUnitContext) { + if (!adUnitContext || !adUnitContext.refererInfo) return null; + + const domain = extractSiteDomainFromURL(adUnitContext.refererInfo.referer); + + if (!domain) return null; + + return { + page: adUnitContext.refererInfo.referer, + domain: domain, + name: domain + }; +} + +function extractSiteDomainFromURL(url) { + if (!url || !isStr(url)) return null; + + const domain = url.match(DOMAIN_REGEX); + + if (isArray(domain) && domain.length === 2) return domain[1]; + + return null; +} + +function getDeviceInfo() { + return {ua: navigator.userAgent, language: navigator.language}; +} + +function getUserInfoFromAdUnitContext(adUnitContext) { + if (!adUnitContext || !adUnitContext.gdprConsent) return null; + if (!isStr(adUnitContext.gdprConsent.consentString)) return null; + + return { + ext: { + consent: adUnitContext.gdprConsent.consentString + } + }; +} + +function getRegulationFromAdUnitContext(adUnitContext) { + if (!adUnitContext || !adUnitContext.gdprConsent) return null; + if (!isBoolean(adUnitContext.gdprConsent.gdprApplies)) return null; + + return { + ext: { + gdpr: adUnitContext.gdprConsent.gdprApplies + } + }; +} + +function generateBidRequestExtension() { + return { + adot: {adapter_version: ADAPTER_VERSION}, + should_use_gzip: true + }; +} + +function validateServerResponse(serverResponse) { + return isPlainObject(serverResponse) && + isPlainObject(serverResponse.body) && + isStr(serverResponse.body.cur) && + isArray(serverResponse.body.seatbid); +} + +function seatBidsToAds(seatBid, bidResponse, serverRequest) { + return seatBid.bid + .filter(bid => validateBids(bid, serverRequest)) + .map(bid => generateAdFromBid(bid, bidResponse, serverRequest)); +} + +function validateBids(bid, serverRequest) { + if (!isPlainObject(bid)) return false; + if (!isStr(bid.impid)) return false; + if (!isStr(bid.crid)) return false; + if (!isNumber(bid.price)) return false; + + if (!isPlainObject(bid.ext)) return false; + if (!isPlainObject(bid.ext.adot)) return false; + if (!isStr(bid.ext.adot.media_type)) return false; + if (BID_SUPPORTED_MEDIA_TYPES.indexOf(bid.ext.adot.media_type) === -1) return false; + + if (!bid.adm && !bid.nurl) return false; + if (bid.adm) { + if (!isStr(bid.adm)) return false; + if (bid.adm.indexOf(AUCTION_PRICE) === -1) return false; + } + if (bid.nurl) { + if (!isStr(bid.nurl)) return false; + if (bid.nurl.indexOf(AUCTION_PRICE) === -1) return false; + } + + if (isBidABanner(bid)) { + if (!isNumber(bid.h)) return false; + if (!isNumber(bid.w)) return false; + } + if (isBidAVideo(bid)) { + if (!(isNone(bid.h) || isNumber(bid.h))) return false; + if (!(isNone(bid.w) || isNumber(bid.w))) return false; + } + + const impression = getImpressionData(serverRequest, bid.impid); + + if (!isPlainObject(impression.openRTB)) return false; + if (!isPlainObject(impression.internal)) return false; + if (!isStr(impression.internal.adUnitCode)) return false; + + if (isBidABanner(bid)) { + if (!isPlainObject(impression.openRTB.banner)) return false; + } + if (isBidAVideo(bid)) { + if (!isPlainObject(impression.openRTB.video)) return false; + } + if (isBidANative(bid)) { + if (!isPlainObject(impression.openRTB.native) || !tryParse(bid.adm)) return false; + } + + return true; +} + +function isBidABanner(bid) { + return isPlainObject(bid) && + isPlainObject(bid.ext) && + isPlainObject(bid.ext.adot) && + bid.ext.adot.media_type === 'banner'; +} + +function isBidAVideo(bid) { + return isPlainObject(bid) && + isPlainObject(bid.ext) && + isPlainObject(bid.ext.adot) && + bid.ext.adot.media_type === 'video'; +} + +function isBidANative(bid) { + return isPlainObject(bid) && + isPlainObject(bid.ext) && + isPlainObject(bid.ext.adot) && + bid.ext.adot.media_type === 'native'; +} + +function getImpressionData(serverRequest, impressionId) { + const openRTBImpression = find(serverRequest.data.imp, imp => imp.id === impressionId); + const internalImpression = find(serverRequest._adot_internal.impressions, imp => imp.impressionId === impressionId); + + return { + id: impressionId, + openRTB: openRTBImpression || null, + internal: internalImpression || null + }; +} + +function generateAdFromBid(bid, bidResponse, serverRequest) { + const impressionData = getImpressionData(serverRequest, bid.impid); + const isVideo = isBidAVideo(bid); + const base = { + requestId: impressionData.internal.bidId, + cpm: bid.price, + currency: bidResponse.cur, + ttl: TTL, + creativeId: bid.crid, + netRevenue: NET_REVENUE, + mediaType: bid.ext.adot.media_type, + }; + + if (isBidANative(bid)) return {...base, native: formatNativeData(bid.adm)}; + + const size = getSizeFromBid(bid, impressionData); + const creative = getCreativeFromBid(bid, impressionData); + + return { + ...base, + height: size.height, + width: size.width, + ad: creative.markup, + adUrl: creative.markupUrl, + vastXml: isVideo && !isStr(creative.markupUrl) ? creative.markup : null, + vastUrl: isVideo && isStr(creative.markupUrl) ? creative.markupUrl : null, + renderer: creative.renderer + }; +} + +function formatNativeData(adm) { + const parsedAdm = tryParse(adm); + const {assets, link: {url, clicktrackers}, imptrackers, jstracker} = parsedAdm.native; + const placements = NATIVE_PLACEMENTS; + const placementIds = NATIVE_ID_MAPPING; + + return assets.reduce((acc, asset) => { + const placementName = placementIds[asset.id]; + const content = placementName && asset[placements[placementName].name]; + if (!content) return acc; + acc[placementName] = content.text || content.value || {url: content.url, width: content.w, height: content.h}; + return acc; + }, { + clickUrl: url, + clickTrackers: clicktrackers, + impressionTrackers: imptrackers, + javascriptTrackers: jstracker && [jstracker] + }); +} + +function getSizeFromBid(bid, impressionData) { + if (isNumber(bid.w) && isNumber(bid.h)) { + return { width: bid.w, height: bid.h }; + } + + if (isImpressionAVideo(impressionData.openRTB)) { + const { video } = impressionData.openRTB; + + if (isNumber(video.w) && isNumber(video.h)) { + return { width: video.w, height: video.h }; + } + } + + return { width: null, height: null }; +} + +function getCreativeFromBid(bid, impressionData) { + const shouldUseAdMarkup = !!bid.adm; + + return { + markup: shouldUseAdMarkup ? bid.adm : null, + markupUrl: !shouldUseAdMarkup ? bid.nurl : null, + renderer: getRendererFromBid(bid, impressionData) + }; +} + +function getRendererFromBid(bid, impressionData) { + const isOutstreamImpression = isBidAVideo(bid) && + isImpressionAVideo(impressionData.openRTB) && + isOutstream(impressionData.openRTB.video); + + return isOutstreamImpression + ? buildOutstreamRenderer(impressionData) + : null; +} + +function buildOutstreamRenderer(impressionData) { + const renderer = Renderer.install({ + url: OUTSTREAM_VIDEO_PLAYER_URL, + loaded: false, + adUnitCode: impressionData.internal.adUnitCode + }); + + renderer.setRender((ad) => { + ad.renderer.push(() => { + const container = impressionData.internal.container + ? document.querySelector(impressionData.internal.container) + : document.getElementById(impressionData.internal.adUnitCode); + + const player = new window.VASTPlayer(container); + + player.on('ready', () => { + player.adVolume = 0; + player.startAd(); + }); + + try { + isStr(ad.adUrl) + ? player.load(ad.adUrl) + : player.loadXml(ad.ad) + } catch (err) { + logError(err); + } + }); + }); + + return renderer; +} + +function tryParse(data) { + try { + return JSON.parse(data); + } catch (err) { + logError(err); + return null; + } +} + +const adotBidderSpec = { + code: 'adot', + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + isBidRequestValid(adUnit) { + const allowedBidderCodes = [this.code]; + + return isPlainObject(adUnit) && + allowedBidderCodes.indexOf(adUnit.bidder) !== -1 && + isStr(adUnit.adUnitCode) && + isStr(adUnit.bidderRequestId) && + isStr(adUnit.bidId) && + validateMediaTypes(adUnit.mediaTypes, this.supportedMediaTypes) && + validateParameters(adUnit.params, adUnit); + }, + buildRequests(adUnits, adUnitContext) { + if (!adUnits) return null; + + return groupBy(adUnits, 'bidderRequestId').map(group => { + const bidRequestId = group.id; + const adUnits = groupBy(group.values, 'bidId').map((group) => { + const length = group.values.length; + return length > 0 && group.values[length - 1] + }); + + return createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) + }); + }, + interpretResponse(serverResponse, serverRequest) { + if (!validateServerRequest(serverRequest)) return []; + if (!validateServerResponse(serverResponse)) return []; + + const bidResponse = serverResponse.body; + + return bidResponse.seatbid + .filter(seatBid => isPlainObject(seatBid) && isArray(seatBid.bid)) + .reduce((acc, seatBid) => acc.concat(seatBidsToAds(seatBid, bidResponse, serverRequest)), []); + } +}; + +registerBidder(adotBidderSpec); + +export {adotBidderSpec as spec}; diff --git a/modules/adotBidAdapter.md b/modules/adotBidAdapter.md new file mode 100644 index 00000000000..88c8fb0b936 --- /dev/null +++ b/modules/adotBidAdapter.md @@ -0,0 +1,218 @@ +# Adot Bidder Adapter + +Adot Bidder Adapter is a module that enables the communication between the Prebid.js library and Adot's DSP. + +## Overview + +- Module name: Adot Bidder Adapter +- Module type: Bidder Adapter +- Maintainer: `maxime.lequain@we-are-adot.com` +- Supported media types: `banner`, `video`, `native` + +## Example ad units + +### Banner ad unit + +Adot Bidder Adapter accepts banner ad units using the following ad unit format: + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + banner: { + // Dimensions supported by the banner ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'adot', + params: {} + }] +} +``` + +### Video ad unit + +#### Outstream video ad unit + +Adot Bidder Adapter accepts outstream video ad units using the following ad unit format: + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + video: { + // Video context. Must be 'outstream'. + context: 'outstream', + // Video dimensions supported by the video ad unit. + // Each ad unit size is formatted as follows: [width, height]. + playerSize: [[300, 250]] + } + }, + bids: [{ + bidder: 'adot', + params: { + video: { + // Content MIME types supported by the ad unit. + mimes: ['video/mp4'], + // Minimum accepted video ad duration (in seconds). + minDuration: 5, + // Maximum accepted video ad duration (in seconds). + maxDuration: 35, + // Video protocols supported by the ad unit (see the OpenRTB 2.5 specifications, + // section 5.8). + protocols: [2, 3] + } + } + }] +} +``` + +#### Instream video ad unit + +Adot Bidder Adapter accepts instream video ad units using the following ad unit format: + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + video: { + // Video context. Must be 'instream'. + context: 'instream', + // Video dimensions supported by the video ad unit. + // Each ad unit size is formatted as follows: [width, height]. + playerSize: [[300, 250]] + } + }, + bids: [{ + bidder: 'adot', + params: { + video: { + // Content MIME types supported by the ad unit. + mimes: ['video/mp4'], + // Minimum accepted video ad duration (in seconds). + minDuration: 5, + // Maximum accepted video ad duration (in seconds). + maxDuration: 35, + // Video protocols supported by the ad unit (see the OpenRTB 2.5 specifications, + // section 5.8). + protocols: [2, 3], + // Instream video context. Must be either 'pre-roll', 'mid-roll' or 'post-roll'. + instreamContext: 'pre-roll' + } + } + }] +} +``` +### Native ad unit + +Adot Bidder Adapter accepts native ad units using the following ad unit format: + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + native: { + native: { + image: { + // Field required status + required: false, + // Image dimensions supported by the native ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [100, 50] + }, + title: { + // Field required status + required: false, + // Maximum length of the title + len: 140 + }, + sponsoredBy: { + // Field required status + required: false + }, + clickUrl: { + // Field required status + required: false + }, + body: { + // Field required status + required: false + }, + icon: { + // Field required status + required: false, + // Icon dimensions supported by the native ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [50, 50] + } + } + } + }, + bids: [{ + bidder: 'adot', + params: {} + }] +} +``` + +## Bidder parameters + +### Position + +You can use the `position` to specify the position of the ad as a relative measure of visibility or prominence. + +#### Accepted values + +|Value|Description | +|-----|----------------| +|0 | Unknown | +|1 | Above the fold | +|3 | Below the fold | + +Note that the position will default to `0` if the field is missing. + +#### Example + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + banner: { + // Dimensions supported by the banner ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'adot', + params: { + position: 0 + } + }] +} +``` + +### PlacementId + +#### Example + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + banner: { + // Dimensions supported by the banner ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'adot', + params: { + placementId: 'eae245d' + } + }] +} +``` diff --git a/test/spec/modules/adotBidAdapter_spec.js b/test/spec/modules/adotBidAdapter_spec.js new file mode 100644 index 00000000000..594fc4ac7b7 --- /dev/null +++ b/test/spec/modules/adotBidAdapter_spec.js @@ -0,0 +1,3118 @@ +import { expect } from 'chai'; +import { executeRenderer } from 'src/Renderer.js'; +import * as utils from 'src/utils.js'; +import { spec } from 'modules/adotBidAdapter.js'; + +const BIDDER_URL = 'https://dsp.adotmob.com/headerbidding/bidrequest'; + +describe('Adot Adapter', function () { + const examples = { + adUnit_banner: { + adUnitCode: 'ad_unit_banner', + bidder: 'adot', + bidderRequestId: 'bid_request_id', + bidId: 'bid_id', + params: {}, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }, + + adUnit_video_outstream: { + adUnitCode: 'ad_unit_video_outstream', + bidder: 'adot', + bidderRequestId: 'bid_request_id', + bidId: 'bid_id', + params: { + video: { + mimes: ['video/mp4'], + minDuration: 5, + maxDuration: 30, + protocols: [2, 3] + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[300, 250]] + } + } + }, + + adUnit_video_instream: { + adUnitCode: 'ad_unit_video_instream', + bidder: 'adot', + bidderRequestId: 'bid_request_id', + bidId: 'bid_id', + params: { + video: { + instreamContext: 'pre-roll', + mimes: ['video/mp4'], + minDuration: 5, + maxDuration: 30, + protocols: [2, 3] + } + }, + mediaTypes: { + video: { + context: 'instream', + playerSize: [[300, 250]] + } + } + }, + + adUnitContext: { + refererInfo: { + referer: 'https://we-are-adot.com/test', + }, + gdprConsent: { + consentString: 'consent_string', + gdprApplies: true + } + }, + + adUnit_position: { + adUnitCode: 'ad_unit_position', + bidder: 'adot', + bidderRequestId: 'bid_request_id', + bidId: 'bid_id', + params: { + position: 1 + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }, + + adUnit_native: { + adUnitCode: 'ad_unit_native', + bidder: 'adot', + bidderRequestId: 'bid_request_id', + bidId: 'bid_id', + params: {}, + mediaTypes: { + native: { + title: {required: true, len: 140}, + icon: {required: true, sizes: [50, 50]}, + image: {required: false, sizes: [320, 200]}, + sponsoredBy: {required: false}, + body: {required: false}, + cta: {required: true} + } + } + }, + + serverRequest_banner: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_banner_0_0', + banner: { + format: [{ + w: 300, + h: 200 + }] + }, + video: null + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_banner_0_0', + adUnitCode: 'ad_unit_banner', + bidId: 'imp_id_banner' + } + ] + } + }, + + serverRequest_banner_twoImps: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_banner_0_0', + banner: { + format: [{ + w: 300, + h: 200 + }] + }, + video: null + }, + { + id: 'imp_id_banner_2_0_0', + banner: { + format: [{ + w: 300, + h: 200 + }] + }, + video: null + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_banner_0_0', + adUnitCode: 'ad_unit_banner', + bidId: 'imp_id_banner' + }, + { + impressionId: 'imp_id_banner_2_0_0', + adUnitCode: 'ad_unit_banner_2', + bidId: 'imp_id_banner_2' + } + ] + } + }, + + serverRequest_video_instream: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_video_instream_0', + banner: null, + video: { + mimes: ['video/mp4'], + w: 300, + h: 200, + startdelay: 0, + minduration: 5, + maxduration: 35, + protocols: [2, 3] + } + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_video_instream_0', + adUnitCode: 'ad_unit_video_instream', + bidId: 'imp_id_video_instream' + } + ] + } + }, + + serverRequest_video_outstream: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_video_outstream_0', + banner: null, + video: { + mimes: ['video/mp4'], + w: 300, + h: 200, + startdelay: null, + minduration: 5, + maxduration: 35, + protocols: [2, 3] + } + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_video_outstream_0', + adUnitCode: 'ad_unit_video_outstream', + bidId: 'imp_id_video_outstream' + } + ] + } + }, + + serverRequest_video_instream_outstream: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_video_instream_0', + banner: null, + video: { + mimes: ['video/mp4'], + w: 300, + h: 200, + startdelay: 0, + minduration: 5, + maxduration: 35, + protocols: [2, 3] + } + }, + { + id: 'imp_id_video_outstream_0', + banner: null, + video: { + mimes: ['video/mp4'], + w: 300, + h: 200, + startdelay: null, + minduration: 5, + maxduration: 35, + protocols: [2, 3] + } + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_video_instream_0', + adUnitCode: 'ad_unit_video_instream', + bidId: 'imp_id_video_instream' + }, + { + impressionId: 'imp_id_video_outstream_0', + adUnitCode: 'ad_unit_video_outstream', + bidId: 'imp_id_video_outstream' + } + ] + } + }, + + serverRequest_position: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_banner', + banner: { + format: [{ + w: 300, + h: 200 + }], + position: 1 + }, + video: null + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_banner', + adUnitCode: 'ad_unit_position' + } + ] + } + }, + + serverRequest_native: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_native_0', + native: { + request: { + assets: [ + { + id: 1, + required: true, + title: { + len: 140 + } + }, + { + id: 2, + required: true, + img: { + type: 1, + wmin: 50, + hmin: 50 + } + }, + { + id: 3, + required: false, + img: { + type: 3, + wmin: 320, + hmin: 200 + } + }, + { + id: 4, + required: false, + data: { + type: 1 + } + }, + { + id: 5, + required: false, + data: { + type: 2 + } + }, + { + id: 6, + required: true, + data: { + type: 12 + } + } + ] + } + }, + video: null, + banner: null + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_native_0', + adUnitCode: 'ad_unit_native', + bidId: 'imp_id_native' + } + ] + } + }, + + serverResponse_banner: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_banner_0_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + h: 350, + w: 300, + ext: { + adot: { + media_type: 'banner' + } + } + } + ] + } + ] + } + }, + + serverResponse_banner_twoBids: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_banner_0_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + h: 350, + w: 300, + ext: { + adot: { + media_type: 'banner' + } + } + }, + { + impid: 'imp_id_banner_2_0_0', + crid: 'creative_id_2', + adm: 'creative_data_2_${AUCTION_PRICE}', + nurl: 'win_notice_url_2_${AUCTION_PRICE}', + price: 2.5, + h: 400, + w: 350, + ext: { + adot: { + media_type: 'banner' + } + } + } + ] + } + ] + } + }, + + serverResponse_video_instream: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_video_instream_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + ext: { + adot: { + media_type: 'video' + } + } + } + ] + } + ] + } + }, + + serverResponse_video_outstream: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_video_outstream_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + ext: { + adot: { + media_type: 'video' + } + } + } + ] + } + ] + } + }, + + serverResponse_video_instream_outstream: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_video_instream_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + ext: { + adot: { + media_type: 'video' + } + } + }, + { + impid: 'imp_id_video_outstream_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + ext: { + adot: { + media_type: 'video' + } + } + } + ] + } + ] + } + }, + + serverResponse_native: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_native_0', + crid: 'creative_id', + adm: '{"native":{"assets":[{"id":1,"title":{"len":140,"text":"Hi everyone"}},{"id":2,"img":{"url":"https://adotmob.com","type":1,"w":50,"h":50}},{"id":3,"img":{"url":"https://adotmob.com","type":3,"w":320,"h":200}},{"id":4,"data":{"type":1,"value":"adotmob"}},{"id":5,"data":{"type":2,"value":"This is a test ad"}},{"id":6,"data":{"type":12,"value":"Click to buy"}}],"link":{"url":"https://adotmob.com?auction=${AUCTION_PRICE}"}}}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + ext: { + adot: { + media_type: 'native' + } + } + } + ] + } + ] + } + } + }; + + describe('isBidRequestValid', function () { + describe('General', function () { + it('should return false when not given an ad unit', function () { + const adUnit = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an invalid ad unit', function () { + const adUnit = 'bad_bid'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without bidder code', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidder = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a bad bidder code', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidder = 'unknownBidder'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without ad unit code', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.adUnitCode = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid ad unit code', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.adUnitCode = {}; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without bid request identifier', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidderRequestId = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid bid request identifier', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidderRequestId = {}; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without impression identifier', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidId = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid impression identifier', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidId = {}; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without media types', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with empty media types', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes = {}; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with invalid media types', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes = 'bad_media_types'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + }); + + describe('Banner', function () { + it('should return true when given a valid ad unit', function () { + const adUnit = examples.adUnit_banner; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given a valid ad unit without bidder parameters', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.params = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return false when given an ad unit without size', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid size', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = 'bad_banner_size'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an empty size', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = []; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid size value', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = ['bad_banner_size_value']; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a size value with less than 2 dimensions', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[300]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a size value with more than 2 dimensions', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[300, 250, 30]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a negative width value', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[-300, 250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a negative height value', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[300, -250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid width value', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[false, 250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid height value', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[300, {}]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + }); + + describe('Video', function () { + it('should return true when given a valid outstream ad unit', function () { + const adUnit = examples.adUnit_video_outstream; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given a valid pre-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'pre-roll'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given a valid mid-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'mid-roll'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given a valid post-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'post-roll'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given an ad unit without size', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given an ad unit with an empty size', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = []; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given an ad unit without minimum duration parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.minDuration = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given an ad unit without maximum duration parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.maxDuration = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return false when given an ad unit without bidder parameters', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with invalid bidder parameters', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params = 'bad_bidder_parameters'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without video parameters', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with invalid video parameters', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video = 'bad_bidder_parameters'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without mime types parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.mimes = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid mime types parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.mimes = 'bad_mime_types'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an empty mime types parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.mimes = []; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid mime types parameter value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.mimes = [200]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid minimum duration parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.minDuration = 'bad_min_duration'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid maximum duration parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.maxDuration = 'bad_max_duration'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without protocols parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.protocols = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid protocols parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.protocols = 'bad_protocols'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an empty protocols parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.protocols = []; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid protocols parameter value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.protocols = ['bad_protocols_value']; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an instream ad unit without instream context', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an instream ad unit with an invalid instream context', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'bad_instream_context'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without context', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.context = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid context', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.context = []; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an adpod ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.context = 'adpod'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an unknown context', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.context = 'invalid_context'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid size', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = 'bad_video_size'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid size value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = ['bad_video_size_value']; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a size value with less than 2 dimensions', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[300]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a size value with more than 2 dimensions', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[300, 250, 30]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a negative width value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[-300, 250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a negative height value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[300, -250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid width value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[false, 250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid height value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[300, {}]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + }); + }); + + describe('buildRequests', function () { + describe('ServerRequest', function () { + it('should return a server request when given a valid ad unit and a valid ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.length(1); + expect(serverRequests[0].method).to.exist.and.to.be.a('string').and.to.equal('POST'); + expect(serverRequests[0].url).to.exist.and.to.be.a('string').and.to.equal(BIDDER_URL); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions).to.exist.and.to.be.an('array').and.to.have.length(1); + expect(serverRequests[0]._adot_internal.impressions[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions[0].impressionId).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0]._adot_internal.impressions[0].adUnitCode).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].adUnitCode); + }); + + it('should return a server request containing a position when given a valid ad unit and a valid ad unit context and a position in the bidder params', function () { + const adUnits = [examples.adUnit_position]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.length(1); + expect(serverRequests[0].method).to.exist.and.to.be.a('string').and.to.equal('POST'); + expect(serverRequests[0].url).to.exist.and.to.be.a('string').and.to.equal(BIDDER_URL); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions).to.exist.and.to.be.an('array').and.to.have.length(1); + expect(serverRequests[0]._adot_internal.impressions[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions[0].impressionId).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0]._adot_internal.impressions[0].adUnitCode).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].adUnitCode); + expect(serverRequests[0].data.imp[0].banner.pos).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.position); + }); + + it('should return a server request when given two valid ad units and a valid ad unit context', function () { + const adUnits_1 = utils.deepClone(examples.adUnit_banner); + adUnits_1.bidId = 'bid_id_1'; + adUnits_1.adUnitCode = 'ad_unit_banner_1'; + + const adUnits_2 = utils.deepClone(examples.adUnit_banner); + adUnits_2.bidId = 'bid_id_2'; + adUnits_2.adUnitCode = 'ad_unit_banner_2'; + + const adUnits = [adUnits_1, adUnits_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.length(1); + expect(serverRequests[0].method).to.exist.and.to.be.a('string').and.to.equal('POST'); + expect(serverRequests[0].url).to.exist.and.to.be.a('string').and.to.equal(BIDDER_URL); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions).to.exist.and.to.be.an('array').and.to.have.length(2); + expect(serverRequests[0]._adot_internal.impressions[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions[0].impressionId).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0]._adot_internal.impressions[0].adUnitCode).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].adUnitCode); + expect(serverRequests[0]._adot_internal.impressions[1]).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions[1].impressionId).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0_0`); + expect(serverRequests[0]._adot_internal.impressions[1].adUnitCode).to.exist.and.to.be.a('string').and.to.equal(adUnits[1].adUnitCode); + }); + + it('should return an empty server request list when given an empty ad unit list and a valid ad unit context', function () { + const adUnits = []; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.length(0); + }); + + it('should not return a server request when given no ad unit and a valid ad unit context', function () { + const serverRequests = spec.buildRequests(null, examples.adUnitContext); + + expect(serverRequests).to.equal(null); + }); + + it('should not return a server request when given a valid ad unit and no ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, null); + + expect(serverRequests).to.be.an('array').and.to.have.length(1); + }); + + it('should not return a server request when given a valid ad unit and an invalid ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, {}); + + expect(serverRequests).to.be.an('array').and.to.have.length(1); + }); + }); + + describe('BidRequest', function () { + it('should return a valid server request when given a valid ad unit', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.at).to.exist.and.to.be.a('number').and.to.equal(1); + expect(serverRequests[0].data.ext).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.ext.adot).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.ext.adot.adapter_version).to.exist.and.to.be.a('string').and.to.equal('v1.0.0'); + }); + + it('should return one server request when given one valid ad unit', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].bidderRequestId); + }); + + it('should return one server request when given two valid ad units with different impression identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_banner); + adUnit_1.bidId = 'bid_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_banner); + adUnit_2.bidId = 'bid_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[1].bidderRequestId); + }); + + it('should return two server requests when given two valid ad units with different bid request identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_banner); + adUnit_1.bidderRequestId = 'bidder_request_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_banner); + adUnit_2.bidderRequestId = 'bidder_request_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(2); + expect(serverRequests[0].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[1].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[1].bidderRequestId); + }); + }); + + describe('Impression', function () { + describe('Banner', function () { + it('should return a server request with one impression when given a valid ad unit', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[0].banner.format).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[0].mediaTypes.banner.sizes[0][0], + h: adUnits[0].mediaTypes.banner.sizes[0][1] + }); + }); + + it('should return a server request with two impressions containing one banner formats when given a valid ad unit with two banner sizes', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [ + [300, 250], + [350, 300] + ]; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(2); + + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[0].banner.format).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[0].mediaTypes.banner.sizes[0][0], + h: adUnits[0].mediaTypes.banner.sizes[0][1] + }); + + expect(serverRequests[0].data.imp[1]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_1`); + expect(serverRequests[0].data.imp[1].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[1][0]); + expect(serverRequests[0].data.imp[1].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[1][1]); + expect(serverRequests[0].data.imp[1].banner.format).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[1].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[0].mediaTypes.banner.sizes[1][0], + h: adUnits[0].mediaTypes.banner.sizes[1][1] + }); + }); + + it('should return a server request with two impressions when given two valid ad units with different impression identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_banner); + adUnit_1.bidId = 'bid_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_banner); + adUnit_2.bidId = 'bid_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(2); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[0].banner.format).to.exist.and.to.be.an('array'); + expect(serverRequests[0].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[0].mediaTypes.banner.sizes[0][0], + h: adUnits[0].mediaTypes.banner.sizes[0][1] + }); + expect(serverRequests[0].data.imp[1]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0_0`); + expect(serverRequests[0].data.imp[1].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[1].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[1].banner.format).to.exist.and.to.be.an('array'); + expect(serverRequests[0].data.imp[1].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[1].mediaTypes.banner.sizes[0][0], + h: adUnits[1].mediaTypes.banner.sizes[0][1] + }); + }); + + it('should return a server request with one overriden impression when given two valid ad units with identical identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_banner); + adUnit_1.mediaTypes.banner.sizes = [[300, 250]]; + + const adUnit_2 = utils.deepClone(examples.adUnit_banner); + adUnit_2.mediaTypes.banner.sizes = [[350, 300]]; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0_0`); + expect(serverRequests[0].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[0].banner.format).to.exist.and.to.be.an('array'); + expect(serverRequests[0].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[1].mediaTypes.banner.sizes[0][0], + h: adUnits[1].mediaTypes.banner.sizes[0][1] + }); + }); + + it('should return two server requests with one impression when given two valid ad units with different bid request identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_banner); + adUnit_1.bidderRequestId = 'bidder_request_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_banner); + adUnit_2.bidderRequestId = 'bidder_request_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(2); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[0].banner.format).to.exist.and.to.be.an('array'); + expect(serverRequests[0].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[0].mediaTypes.banner.sizes[0][0], + h: adUnits[0].mediaTypes.banner.sizes[0][1] + }); + expect(serverRequests[1].data).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[1].bidderRequestId); + expect(serverRequests[1].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[1].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0_0`); + expect(serverRequests[1].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[1].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[1].data.imp[0].banner.format).to.exist.and.to.be.an('array'); + expect(serverRequests[1].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[1].mediaTypes.banner.sizes[0][0], + h: adUnits[1].mediaTypes.banner.sizes[0][1] + }); + }); + }); + + describe('Video', function () { + it('should return a server request with one impression when given a valid outstream ad unit', function () { + const adUnit = examples.adUnit_video_outstream; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid pre-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'pre-roll'; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.exist.and.to.be.a('number').and.to.equal(0); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid mid-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'mid-roll'; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.exist.and.to.be.a('number').and.to.equal(-1); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid post-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'post-roll'; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.exist.and.to.be.a('number').and.to.equal(-2); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid ad unit without player size', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = undefined; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.equal(null); + expect(serverRequests[0].data.imp[0].video.h).to.equal(null); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid ad unit with an empty player size', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = []; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.equal(null); + expect(serverRequests[0].data.imp[0].video.h).to.equal(null); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid ad unit with multiple player sizes', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[350, 300], [400, 350]]; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid ad unit without minimum duration', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.minDuration = undefined; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.equal(null); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid ad unit without maximum duration', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.maxDuration = undefined; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.equal(null); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with two impressions when given two valid ad units with different impression identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_1.bidId = 'bid_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_2.bidId = 'bid_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(2); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + expect(serverRequests[0].data.imp[1]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0`); + expect(serverRequests[0].data.imp[1].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[1].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[1].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[1].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[1].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.minDuration); + expect(serverRequests[0].data.imp[1].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.maxDuration); + expect(serverRequests[0].data.imp[1].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[1].params.video.protocols); + }); + + it('should return a server request with one overridden impression when given two valid ad units with identical identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_1.params.video.minDuration = 10; + + const adUnit_2 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_2.params.video.minDuration = 15; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[1].params.video.protocols); + }); + + it('should return two server requests with one impression when given two valid ad units with different bid request identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_1.bidderRequestId = 'bidder_request_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_2.bidderRequestId = 'bidder_request_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(2); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + expect(serverRequests[1].data).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[1].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0`); + expect(serverRequests[1].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[1].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[1].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[1].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[1].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.minDuration); + expect(serverRequests[1].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.maxDuration); + expect(serverRequests[1].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[1].params.video.protocols); + }); + }); + + describe('Native', function () { + it('should return a server request with one impression when given a valid ad unit', function () { + const adUnits = [examples.adUnit_native]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnit_native); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].native).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].native.request).to.exist.and.to.be.a('string').and.to.equal(JSON.stringify(examples.serverRequest_native.data.imp[0].native.request)) + }); + }); + }); + + describe('Site', function () { + it('should return a server request with site information when given a valid ad unit and a valid ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = examples.adUnitContext; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.site.page).to.exist.and.to.be.an('string').and.to.equal(adUnitContext.refererInfo.referer); + expect(serverRequests[0].data.site.id).to.equal(undefined); + expect(serverRequests[0].data.site.domain).to.exist.and.to.be.an('string').and.to.equal('we-are-adot.com'); + expect(serverRequests[0].data.site.name).to.exist.and.to.be.an('string').and.to.equal('we-are-adot.com'); + }); + + it('should return a server request without site information when not given an ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + + it('should return a server request without site information when given an ad unit context without referer information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.refererInfo = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + + it('should return a server request without site information when given an ad unit context with invalid referer information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.refererInfo = 'bad_referer_information'; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + + it('should return a server request without site information when given an ad unit context without referer', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.refererInfo.referer = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + + it('should return a server request without site information when given an ad unit context with an invalid referer', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.refererInfo.referer = {}; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + + it('should return a server request without site information when given an ad unit context with a misformatted referer', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.refererInfo.referer = 'we-are-adot'; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + }); + + describe('Device', function () { + it('should return a server request with device information when given a valid ad unit and a valid ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.device).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.device.ua).to.exist.and.to.be.a('string'); + expect(serverRequests[0].data.device.language).to.exist.and.to.be.a('string'); + }); + }); + + describe('Regs', function () { + it('should return a server request with regulations information when given a valid ad unit and a valid ad unit context with GDPR applying', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = examples.adUnitContext; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.regs.ext).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.regs.ext.gdpr).to.exist.and.to.be.a('boolean').and.to.equal(adUnitContext.gdprConsent.gdprApplies); + }); + + it('should return a server request with regulations information when given a valid ad unit and a valid ad unit context with GDPR not applying', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent.gdprApplies = false; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.regs.ext).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.regs.ext.gdpr).to.exist.and.to.be.a('boolean').and.to.equal(adUnitContext.gdprConsent.gdprApplies); + }); + + it('should return a server request without regulations information when not given an ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.equal(null); + }); + + it('should return a server request without regulations information when given an ad unit context without GDPR information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.equal(null); + }); + + it('should return a server request without regulations information when given an ad unit context with invalid GDPR information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent = 'bad_gdpr_consent'; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.equal(null); + }); + + it('should return a server request without regulations information when given an ad unit context with invalid GDPR application information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent.gdprApplies = 'bad_gdpr_applies'; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.equal(null); + }); + }); + + describe('User', function () { + it('should return a server request with user information when given a valid ad unit and a valid ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = examples.adUnitContext; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.user).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.user.ext).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.user.ext.consent).to.exist.and.to.be.a('string').and.to.equal(adUnitContext.gdprConsent.consentString); + }); + + it('should return a server request without user information when not given an ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.user).to.equal(null); + }); + + it('should return a server request without user information when given an ad unit context without GDPR information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.user).to.equal(null); + }); + + it('should return a server request without user information when given an ad unit context with invalid GDPR information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent = 'bad_gdpr_consent'; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.user).to.equal(null); + }); + + it('should return a server request without user information when given an ad unit context with an invalid consent string', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent.consentString = true; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.user).to.equal(null); + }); + }); + }); + + describe('interpretResponse', function () { + describe('General', function () { + it('should return an ad when given a valid server response with one bid with USD currency', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.cur = 'USD'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(null); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return two ads when given a valid server response with two bids', function () { + const serverRequest = examples.serverRequest_banner_twoImps; + + const serverResponse = examples.serverResponse_banner_twoBids; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(2); + + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(null); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[0].renderer).to.equal(null); + expect(ads[1].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[1].bidId); + expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[1].adm); + expect(ads[1].adUrl).to.equal(null); + expect(ads[1].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[1].crid); + expect(ads[1].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].price); + expect(ads[1].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[1].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[1].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[1].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].h); + expect(ads[1].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].w); + expect(ads[1].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[1].renderer).to.equal(null); + }); + + it('should return no ad when not given a server response', function () { + const ads = spec.interpretResponse(null); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when not given a server response body', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given an invalid server response body', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body = 'invalid_body'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response without seat bids', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with invalid seat bids', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid = 'invalid_seat_bids'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with an empty seat bids array', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid = []; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with an invalid seat bid', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid = 'invalid_bids'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with an empty bids array', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid = []; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with an invalid bid', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid = ['invalid_bid']; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without currency', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.cur = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid currency', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.cur = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without impression identifier', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].impid = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid impression identifier', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].impid = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without creative identifier', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].crid = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid creative identifier', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].crid = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without ad markup and ad serving URL', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].adm = undefined; + serverResponse.body.seatbid[0].bid[0].nurl = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid ad markup', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].adm = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an ad markup without auction price macro', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].adm = 'creative_data'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid ad serving URL', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].nurl = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an ad serving URL without auction price macro', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].nurl = 'win_notice_url'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without bid price', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].price = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid bid price', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].price = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without extension', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid extension', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext = 'bad_ext'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without adot extension', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext.adot = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid adot extension', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext.adot = 'bad_adot_ext'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without media type', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext.adot.media_type = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid media type', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext.adot.media_type = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an unknown media type', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext.adot.media_type = 'unknown_media_type'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and no server request', function () { + const serverRequest = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and an invalid server request', function () { + const serverRequest = 'bad_server_request'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without bid request', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request with an invalid bid request', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data = 'bad_bid_request'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without impression', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data.imp = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request with an invalid impression field', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data.imp = 'invalid_impressions'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without matching impression', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data.imp[0].id = 'unknown_imp_id'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without internal data', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request with invalid internal data', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal = 'bad_internal_data'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without internal impression data', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal.impressions = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request with invalid internal impression data', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal.impressions = 'bad_internal_impression_data'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without matching internal impression', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal.impressions[0].impressionId = 'unknown_imp_id'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without internal impression ad unit code', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal.impressions[0].adUnitCode = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request with an invalid internal impression ad unit code', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal.impressions[0].adUnitCode = {}; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + }); + + describe('Banner', function () { + it('should return an ad when given a valid server response with one bid', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(null); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with one bid without a win notice URL', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].nurl = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(null); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with one bid using an ad serving URL', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].adm = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.equal(null); + expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return no ad when given a server response with a bid without height', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].h = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid height', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].h = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without width', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].w = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid width', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].w = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without banner impression', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data.imp[0].banner = undefined; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + }); + + describe('Video', function () { + it('should return an ad when given a valid server response with one bid on an instream impression', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = examples.serverResponse_video_instream; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with one bid on an outstream impression', function () { + const serverRequest = examples.serverRequest_video_outstream; + + const serverResponse = examples.serverResponse_video_outstream; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.be.an('object'); + }); + + it('should return two ads when given a valid server response with two bids on both instream and outstream impressions', function () { + const serverRequest = examples.serverRequest_video_instream_outstream; + + const serverResponse = examples.serverResponse_video_instream_outstream; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(2); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + expect(ads[1].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[1].bidId); + expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[1].adm); + expect(ads[1].adUrl).to.equal(null); + expect(ads[1].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[1].vastUrl).to.equal(null); + expect(ads[1].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[1].crid); + expect(ads[1].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].price); + expect(ads[1].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[1].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[1].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[1].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[1].video.w); + expect(ads[1].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[1].renderer).to.be.an('object'); + }); + + it('should return an ad when given a valid server response with one bid without a win notice URL', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].nurl = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with one bid using an ad serving URL', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].adm = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.equal(null); + expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].vastXml).to.equal(null); + expect(ads[0].vastUrl).to.equal(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with a bid with a video height', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].h = 500; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with a bid with a video width', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].w = 500; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with a bid with a video width and height', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].w = 500; + serverResponse.body.seatbid[0].bid[0].h = 400; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response and server request with a video impression without width', function () { + const serverRequest = utils.deepClone(examples.serverRequest_video_instream); + serverRequest.data.imp[0].video.w = null; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(null); + expect(ads[0].width).to.equal(null); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response and server request with a video impression without height', function () { + const serverRequest = utils.deepClone(examples.serverRequest_video_instream); + serverRequest.data.imp[0].video.h = null; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(null); + expect(ads[0].width).to.equal(null); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return no ad when given a server response with a bid with an invalid height', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].h = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid width', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].w = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without video impression', function () { + const serverRequest = utils.deepClone(examples.serverRequest_video_instream); + serverRequest.data.imp[0].video = undefined; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + describe('Outstream renderer', function () { + function spyAdRenderingQueue(ad) { + const spy = sinon.spy(ad.renderer, 'push'); + + this.sinonSpies.push(spy); + } + + function executeAdRenderer(ad, onRendererExecution, done) { + executeRenderer(ad.renderer, ad); + + setTimeout(() => { + try { + onRendererExecution(); + } catch (err) { + done(err); + } + + done() + }, 100); + } + + before('Bind helper functions to the Mocha context', function () { + this.spyAdRenderingQueue = spyAdRenderingQueue.bind(this); + + window.VASTPlayer = function VASTPlayer() {}; + window.VASTPlayer.prototype.loadXml = function loadXml() { + return new Promise((resolve, reject) => resolve()) + }; + window.VASTPlayer.prototype.load = function load() { + return new Promise((resolve, reject) => resolve()) + }; + window.VASTPlayer.prototype.on = function on(event, callback) {}; + window.VASTPlayer.prototype.startAd = function startAd() {}; + }); + + beforeEach('Initialize the Sinon spies list', function () { + this.sinonSpies = []; + }); + + afterEach('Clear the registered Sinon spies', function () { + this.sinonSpies.forEach(spy => spy.restore()); + }); + + after('clear data', () => { + window.VASTPlayer = null; + }); + + it('should return an ad with valid renderer', function () { + const serverRequest = examples.serverRequest_video_outstream; + const serverResponse = examples.serverResponse_video_outstream; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].renderer).to.be.an('object'); + }); + + it('should append a command to the ad rendering queue when executing the renderer', function (done) { + const serverRequest = examples.serverRequest_video_outstream; + const serverResponse = examples.serverResponse_video_outstream; + + const [ad] = spec.interpretResponse(serverResponse, serverRequest); + + this.spyAdRenderingQueue(ad); + + executeAdRenderer(ad, () => { + expect(ad.renderer.push.calledOnce).to.equal(true); + expect(ad.renderer.push.firstCall.args[0]).to.exist.and.to.be.a('function'); + }, done); + }); + }); + }); + + describe('Native', function () { + it('should return an ad when given a valid server response with one bid', function () { + const serverRequest = examples.serverRequest_native; + const serverResponse = examples.serverResponse_native; + const native = JSON.parse(serverResponse.body.seatbid[0].bid[0].adm).native; + const {link, assets} = native; + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('native'); + expect(ads[0].native).to.exist.and.to.be.an('object'); + expect(Object.keys(ads[0].native)).to.have.length(10); + expect(ads[0].native.title).to.equal(assets[0].title.text); + expect(ads[0].native.icon.url).to.equal(assets[1].img.url); + expect(ads[0].native.icon.width).to.equal(assets[1].img.w); + expect(ads[0].native.icon.height).to.equal(assets[1].img.h); + expect(ads[0].native.image.url).to.equal(assets[2].img.url); + expect(ads[0].native.image.width).to.equal(assets[2].img.w); + expect(ads[0].native.image.height).to.equal(assets[2].img.h); + expect(ads[0].native.sponsoredBy).to.equal(assets[3].data.value); + expect(ads[0].native.body).to.equal(assets[4].data.value); + expect(ads[0].native.cta).to.equal(assets[5].data.value); + expect(ads[0].native.clickUrl).to.equal(link.url); + }); + }); + }); +}); From 21d1f53f9d522d805ffdf923aa49e60a0c84cfb2 Mon Sep 17 00:00:00 2001 From: Kanchika - Automatad Date: Wed, 8 Apr 2020 00:04:46 +0530 Subject: [PATCH 70/90] Add Automatad Bid Adapter (#5017) * added automatad bid adapter * added automatad bid adapter readme * added automatad bidder adapter unit test * updated maintainer email id for automatad adapter * refactored automatadBidAdapter js * refactored automatadBidAdapter unit test * refactored automatadBidAdapter unit test * added usersync code to automatad bid adapter * Added unit test for onBidWon in automatadBidAdapter_spec * removed trailing spaces * removed trailing space * changes for getUserSync function * lint error fixes * updated usersync url * additional test for onBidWon function added * added ajax stub in test --- modules/automatadBidAdapter.js | 123 +++++++++++++++ modules/automatadBidAdapter.md | 34 +++++ test/spec/modules/automatadBidAdapter_spec.js | 144 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 modules/automatadBidAdapter.js create mode 100644 modules/automatadBidAdapter.md create mode 100644 test/spec/modules/automatadBidAdapter_spec.js diff --git a/modules/automatadBidAdapter.js b/modules/automatadBidAdapter.js new file mode 100644 index 00000000000..4a9e6b6a1e4 --- /dev/null +++ b/modules/automatadBidAdapter.js @@ -0,0 +1,123 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js' +import * as utils from '../src/utils.js' +import {BANNER} from '../src/mediaTypes.js' +import {ajax} from '../src/ajax.js' + +const BIDDER = 'automatad' + +const ENDPOINT_URL = 'https://rtb2.automatad.com/ortb2' + +const DEFAULT_BID_TTL = 30 +const DEFAULT_CURRENCY = 'USD' +const DEFAULT_NET_REVENUE = true + +export const spec = { + code: BIDDER, + aliases: ['atd'], + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + // will receive request bid. check if have necessary params for bidding + return (bid && bid.hasOwnProperty('params') && bid.params.hasOwnProperty('siteId') && bid.params.hasOwnProperty('placementId') && bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty('banner')) + }, + + buildRequests: function (validBidRequests, bidderRequest) { + if (!validBidRequests || !bidderRequest) { + return + } + + const siteId = validBidRequests[0].params.siteId + const placementId = validBidRequests[0].params.placementId + + const impressions = validBidRequests.map(bidRequest => ({ + id: bidRequest.bidId, + banner: { + format: bidRequest.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1], + })) + }, + })) + + // params from bid request + const openrtbRequest = { + id: validBidRequests[0].auctionId, + imp: impressions, + site: { + id: siteId, + placement: placementId, + domain: window.location.hostname, + page: window.location.href, + ref: bidderRequest.refererInfo ? bidderRequest.refererInfo.referer || null : null, + }, + } + + const payloadString = JSON.stringify(openrtbRequest) + return { + method: 'POST', + url: ENDPOINT_URL + '/resp', + data: payloadString, + options: { + contentType: 'application/json', + withCredentials: false, + crossOrigin: true, + }, + } + }, + + interpretResponse: function (serverResponse, request) { + const bidResponses = [] + const response = (serverResponse || {}).body + + if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) { + response.seatbid[0].bid.forEach(bid => { + bidResponses.push({ + requestId: bid.impid, + cpm: bid.price, + ad: bid.adm, + adDomain: bid.adomain[0], + currency: DEFAULT_CURRENCY, + ttl: DEFAULT_BID_TTL, + creativeId: bid.crid, + width: bid.w, + height: bid.h, + netRevenue: DEFAULT_NET_REVENUE, + nurl: bid.nurl, + }) + }) + } else { + utils.logInfo('automatad :: no valid responses to interpret') + } + + return bidResponses + }, + getUserSyncs: function(syncOptions, serverResponse) { + return [{ + type: 'iframe', + url: 'https://rtb2.automatad.com/ortb2/async_usersync' + }] + }, + onBidWon: function(bid) { + if (!bid.nurl) { return } + const winUrl = bid.nurl.replace( + /\$\{AUCTION_PRICE\}/, + bid.cpm + ).replace( + /\$\{AUCTION_IMP_ID\}/, + bid.requestId + ).replace( + /\$\{AUCTION_CURRENCY\}/, + bid.currency + ).replace( + /\$\{AUCTION_ID\}/, + bid.auctionId + ) + spec.ajaxCall(winUrl, null) + return true + }, + ajaxCall: function(endpoint, data) { + ajax(endpoint, data) + }, + +} +registerBidder(spec) diff --git a/modules/automatadBidAdapter.md b/modules/automatadBidAdapter.md new file mode 100644 index 00000000000..56a4b53c067 --- /dev/null +++ b/modules/automatadBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: Automatad Bid Adapter +Module Type: Bidder Adapter +Maintainer: tech@automatad.com +``` + +# Description + +Connects to automatad exchange for bids. + +automatad bid adapter supports Banner ads. + +# Test Parameters +``` +var adUnits = [ + { + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'automatad', + params: { + siteId: 'someValue', + placementId: 'someValue' + } + }] + } +]; +``` diff --git a/test/spec/modules/automatadBidAdapter_spec.js b/test/spec/modules/automatadBidAdapter_spec.js new file mode 100644 index 00000000000..788fd3aefc4 --- /dev/null +++ b/test/spec/modules/automatadBidAdapter_spec.js @@ -0,0 +1,144 @@ +import { expect } from 'chai' +import { spec } from 'modules/automatadBidAdapter.js' +import { newBidder } from 'src/adapters/bidderFactory.js' + +describe('automatadBidAdapter', function () { + const adapter = newBidder(spec) + + let bidRequest = { + bidder: 'automatad', + params: {siteId: '123ad', placementId: '123abc345'}, + mediaTypes: { + banner: { + sizes: [[300, 600]], + } + }, + adUnitCode: 'some-ad-unit-code', + transactionId: '1465569e-52cc-4c36-88a1-7174cfef4b44', + sizes: [[300, 600]], + bidId: '123abc', + bidderRequestId: '3213887463c059', + auctionId: 'abc-123', + src: 'client', + bidRequestsCount: 1 + } + + let expectedResponse = [{ + 'body': { + 'id': 'abc-123', + 'seatbid': [ + { + 'bid': [ + { + 'adm': '', + 'adomain': [ + 'someAdDomain' + ], + 'crid': 123, + 'h': 600, + 'id': 'bid1', + 'impid': '1', + 'nurl': 'https://example/win', + 'price': 0.5, + 'w': 300 + } + ] + } + ] + } + }] + + describe('codes', function () { + it('should return a bidder code of automatad', function () { + expect(spec.code).to.equal('automatad') + }) + it('should alias atd', function () { + expect(spec.aliases.length > 0 && spec.aliases[0] === 'atd').to.be.true + }) + }) + + describe('isBidRequestValid', function () { + let inValidBid = Object.assign({}, bidRequest) + delete inValidBid.params + it('should return true if all params present', function () { + expect(spec.isBidRequestValid(bidRequest)).to.equal(true) + }) + + it('should return false if any parameter missing', function () { + expect(spec.isBidRequestValid(inValidBid)).to.be.false + }) + }) + + describe('buildRequests', function () { + let req = spec.buildRequests([ bidRequest ], { refererInfo: { } }) + let rdata + + it('should return request object', function () { + expect(req).to.not.be.null + }) + + it('should build request data', function () { + expect(req.data).to.not.be.null + }) + + it('should include one request', function () { + rdata = JSON.parse(req.data) + expect(rdata.imp.length).to.equal(1) + }) + + it('should include media types', function () { + let r = rdata.imp[0] + expect(r.media_types !== null).to.be.true + }) + + it('should include all publisher params', function () { + let r = rdata.imp[0] + expect(r.siteID !== null && r.placementID !== null).to.be.true + }) + }) + + describe('interpretResponse', function () { + it('should get the correct bid response', function () { + let result = spec.interpretResponse(expectedResponse[0]) + expect(result).to.be.an('array').that.is.not.empty + }) + + it('handles empty bid response', function () { + let response = { + body: '' + } + let result = spec.interpretResponse(response) + expect(result.length).to.equal(0) + }) + }) + + describe('getUserSyncs', function () { + it('should return iframe sync', function () { + let sync = spec.getUserSyncs() + expect(sync.length).to.equal(1) + expect(sync[0].type === 'iframe') + expect(typeof sync[0].url === 'string') + }) + }) + + describe('onBidWon', function () { + let serverResponses = spec.interpretResponse(expectedResponse[0]) + let wonbid = serverResponses[0] + let ajaxStub + + beforeEach(() => { + ajaxStub = sinon.stub(spec, 'ajaxCall') + }) + + afterEach(() => { + ajaxStub.restore() + }) + + it('Returns true is nurl is good/not blank', function () { + expect(wonbid.nurl).to.not.equal('') + expect(spec.onBidWon(wonbid)).to.be.true + expect(ajaxStub.calledOnce).to.equal(true) + expect(ajaxStub.firstCall.args[0].indexOf('https://')).to.equal(0) + }) + }) +}) From 6ad085f01303da153a17c1f20a018b4fef0bff4e Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Tue, 7 Apr 2020 15:07:05 -0400 Subject: [PATCH 71/90] update regex in readme (#5088) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be07a27ddc1..0e07521ac3b 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ module.exports = { // override the regular exclusion from above (for being inside node_modules). { test: /.js$/, - include: new RegExp(`\\${path.sep}prebid\.js`), + include: new RegExp(`\\${path.sep}prebid\\.js`), use: { loader: 'babel-loader', // presets and plugins for Prebid.js must be manually specified separate from your other babel rule. From 036d71d329a660a8926ee92cc5c3b6c8f07f4ea3 Mon Sep 17 00:00:00 2001 From: Chris DeGroat Date: Tue, 7 Apr 2020 14:28:19 -0500 Subject: [PATCH 72/90] Prebid adapter for windtalker (#5040) * prebid adatper for windtalker * file extension for imports * fix whitespace in adapter * removed inital value assignment per LGTM request * Minorchange to md file for windtalker * Updated windtalker adapter with tests Co-authored-by: Chris DeGroat --- modules/windtalkerBidAdapter.js | 307 +++++++++++++++ modules/windtalkerBidAdapter.md | 86 +++++ .../spec/modules/windtalkerBidAdapter_spec.js | 348 ++++++++++++++++++ 3 files changed, 741 insertions(+) create mode 100644 modules/windtalkerBidAdapter.js create mode 100644 modules/windtalkerBidAdapter.md create mode 100644 test/spec/modules/windtalkerBidAdapter_spec.js diff --git a/modules/windtalkerBidAdapter.js b/modules/windtalkerBidAdapter.js new file mode 100644 index 00000000000..0c0ef571954 --- /dev/null +++ b/modules/windtalkerBidAdapter.js @@ -0,0 +1,307 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import includes from 'core-js/library/fn/array/includes.js'; + +const NATIVE_DEFAULTS = { + TITLE_LEN: 100, + DESCR_LEN: 200, + SPONSORED_BY_LEN: 50, + IMG_MIN: 150, + ICON_MIN: 50, +}; +const DEFAULT_MIMES = ['video/mp4', 'video/webm', 'application/x-shockwave-flash', 'application/javascript']; +const VIDEO_TARGETING = ['mimes', 'skippable', 'playback_method', 'protocols', 'api']; +const DEFAULT_PROTOCOLS = [2, 3, 5, 6]; +const DEFAULT_APIS = [1, 2]; + +export const spec = { + + code: 'windtalker', + supportedMediaTypes: ['banner', 'native', 'video'], + + isBidRequestValid: bid => ( + !!(bid && bid.params && bid.params.pubId && bid.params.placementId) + ), + buildRequests: (bidRequests, bidderRequest) => { + const request = { + id: bidRequests[0].bidderRequestId, + at: 2, + imp: bidRequests.map(slot => impression(slot)), + site: site(bidRequests), + app: app(bidRequests), + device: device(bidRequests), + }; + applyGdpr(bidderRequest, request); + return { + method: 'POST', + url: 'https://windtalkerdisplay.hb.adp3.net/', + data: JSON.stringify(request), + }; + }, + interpretResponse: (response, request) => ( + bidResponseAvailable(request, response.body) + ), +}; + +function bidResponseAvailable(bidRequest, bidResponse) { + const idToImpMap = {}; + const idToBidMap = {}; + const ortbRequest = parse(bidRequest.data); + ortbRequest.imp.forEach(imp => { + idToImpMap[imp.id] = imp; + }); + if (bidResponse) { + bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => { + idToBidMap[bid.impid] = bid; + })); + } + const bids = []; + Object.keys(idToImpMap).forEach(id => { + if (idToBidMap[id]) { + const bid = {}; + bid.requestId = id; + bid.adId = id; + bid.creativeId = id; + bid.cpm = idToBidMap[id].price; + bid.currency = bidResponse.cur; + bid.ttl = 360; + bid.netRevenue = true; + if (idToImpMap[id]['native']) { + bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); + let nurl = idToBidMap[id].nurl; + nurl = nurl.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); + bid['native']['impressionTrackers'] = [nurl]; + bid.mediaType = 'native'; + } else if (idToImpMap[id]['video']) { + bid.vastUrl = idToBidMap[id].adm; + bid.vastUrl = bid.vastUrl.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + bid.crid = idToBidMap[id].crid; + bid.width = idToImpMap[id].video.w; + bid.height = idToImpMap[id].video.h; + bid.mediaType = 'video'; + } else if (idToImpMap[id]['banner']) { + bid.ad = idToBidMap[id].adm; + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_AD_ID(%7D|\})/gi, idToBidMap[id].adid); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); + bid.width = idToBidMap[id].w; + bid.height = idToBidMap[id].h; + bid.mediaType = 'banner'; + } + bids.push(bid); + } + }); + return bids; +} +function impression(slot) { + return { + id: slot.bidId, + secure: window.location.protocol === 'https:' ? 1 : 0, + 'banner': banner(slot), + 'native': nativeImpression(slot), + 'video': videoImpression(slot), + bidfloor: slot.params.bidFloor || '0.000001', + tagid: slot.params.placementId.toString(), + }; +} + +function banner(slot) { + if (slot.mediaType === 'banner' || utils.deepAccess(slot, 'mediaTypes.banner')) { + const sizes = utils.deepAccess(slot, 'mediaTypes.banner.sizes'); + if (sizes.length > 1) { + let format = []; + for (let f = 0; f < sizes.length; f++) { + format.push({'w': sizes[f][0], 'h': sizes[f][1]}); + } + return {'format': format}; + } else { + return { + w: sizes[0][0], + h: sizes[0][1] + } + } + } + return null; +} + +function videoImpression(slot) { + if (slot.mediaType === 'video' || utils.deepAccess(slot, 'mediaTypes.video')) { + const sizes = utils.deepAccess(slot, 'mediaTypes.video.playerSize'); + const video = { + w: sizes[0][0], + h: sizes[0][1], + mimes: DEFAULT_MIMES, + protocols: DEFAULT_PROTOCOLS, + api: DEFAULT_APIS, + }; + if (slot.params.video) { + Object.keys(slot.params.video).filter(param => includes(VIDEO_TARGETING, param)).forEach(param => video[param] = slot.params.video[param]); + } + return video; + } + return null; +} + +function nativeImpression(slot) { + if (slot.mediaType === 'native' || utils.deepAccess(slot, 'mediaTypes.native')) { + const assets = []; + addAsset(assets, titleAsset(1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); + addAsset(assets, dataAsset(2, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); + addAsset(assets, dataAsset(3, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); + addAsset(assets, imageAsset(4, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN)); + addAsset(assets, imageAsset(5, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); + return { + request: JSON.stringify({ assets }), + ver: '1.1', + }; + } + return null; +} + +function addAsset(assets, asset) { + if (asset) { + assets.push(asset); + } +} + +function titleAsset(id, params, defaultLen) { + if (params) { + return { + id, + required: params.required ? 1 : 0, + title: { + len: params.len || defaultLen, + }, + }; + } + return null; +} + +function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) { + return params ? { + id, + required: params.required ? 1 : 0, + img: { + type, + wmin: params.wmin || defaultMinWidth, + hmin: params.hmin || defaultMinHeight, + } + } : null; +} + +function dataAsset(id, params, type, defaultLen) { + return params ? { + id, + required: params.required ? 1 : 0, + data: { + type, + len: params.len || defaultLen, + } + } : null; +} + +function site(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.pubId : '0'; + const siteId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.siteId : '0'; + const appParams = bidderRequest[0].params.app; + if (!appParams) { + return { + publisher: { + id: pubId.toString(), + domain: window.location.hostname, + }, + id: siteId.toString(), + ref: window.top.document.referrer, + page: window.location.href, + } + } + return null; +} + +function app(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.pubId : '0'; + const appParams = bidderRequest[0].params.app; + if (appParams) { + return { + publisher: { + id: pubId.toString(), + }, + id: appParams.id, + name: appParams.name, + bundle: appParams.bundle, + storeurl: appParams.storeUrl, + domain: appParams.domain, + } + } + return null; +} + +function device(bidderRequest) { + const lat = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.latitude : ''; + const lon = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.longitude : ''; + const ifa = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.ifa : ''; + return { + dnt: utils.getDNT() ? 1 : 0, + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + w: (window.screen.width || window.innerWidth), + h: (window.screen.height || window.innerHeigh), + geo: { + lat: lat, + lon: lon, + }, + ifa: ifa, + }; +} + +function parse(rawResponse) { + try { + if (rawResponse) { + return JSON.parse(rawResponse); + } + } catch (ex) { + utils.logError('windtalker.parse', 'ERROR', ex); + } + return null; +} + +function applyGdpr(bidderRequest, ortbRequest) { + if (bidderRequest && bidderRequest.gdprConsent) { + ortbRequest.regs = { ext: { gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0 } }; + ortbRequest.user = { ext: { consent: bidderRequest.gdprConsent.consentString } }; + } +} + +function nativeResponse(imp, bid) { + if (imp['native']) { + const nativeAd = parse(bid.adm); + const keys = {}; + keys.image = {}; + keys.icon = {}; + if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) { + nativeAd['native'].assets.forEach(asset => { + keys.title = asset.title ? asset.title.text : keys.title; + keys.body = asset.data && asset.id === 2 ? asset.data.value : keys.body; + keys.sponsoredBy = asset.data && asset.id === 3 ? asset.data.value : keys.sponsoredBy; + keys.icon.url = asset.img && asset.id === 4 ? asset.img.url : keys.icon.url; + keys.icon.width = asset.img && asset.id === 4 ? asset.img.w : keys.icon.width; + keys.icon.height = asset.img && asset.id === 4 ? asset.img.h : keys.icon.height; + keys.image.url = asset.img && asset.id === 5 ? asset.img.url : keys.image.url; + keys.image.width = asset.img && asset.id === 5 ? asset.img.w : keys.image.width; + keys.image.height = asset.img && asset.id === 5 ? asset.img.h : keys.image.height; + }); + if (nativeAd['native'].link) { + keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url); + } + return keys; + } + } + return null; +} + +registerBidder(spec); diff --git a/modules/windtalkerBidAdapter.md b/modules/windtalkerBidAdapter.md new file mode 100644 index 00000000000..f7441effc47 --- /dev/null +++ b/modules/windtalkerBidAdapter.md @@ -0,0 +1,86 @@ +# Overview + +**Module Name**: Windtalker Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: corbin@windtalker.io + +# Description + +Connects to Windtalker demand source to fetch bids. +Banner, Native, Video formats are supported. +Please use ```windtalker``` as the bidder code. + +# Test Parameters +``` + var adUnits = [{ + code: 'dfp-native-div', + mediaTypes: { + native: { + title: { + required: true, + len: 75 + }, + image: { + required: true + }, + body: { + len: 200 + }, + icon: { + required: false + } + } + }, + bids: [{ + bidder: 'windtalker', + params: { + pubId: '584971', + siteId: '584971', + placementId: '123', + bidFloor: '0.001', // optional + ifa: 'XXX-XXX', // optional + latitude: '40.712775', // optional + longitude: '-74.005973', // optional + } + }] + }, + { + code: 'dfp-banner-div', + mediaTypes: { + banner: { + sizes: [ + [300, 250],[300,600] + ], + } + }, + bids: [{ + bidder: 'windtalker', + params: { + pubId: '584971', + siteId: '584971', + placementId: '123', + } + }] + }, + { + code: 'dfp-video-div', + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: "instream" + } + }, + bids: [{ + bidder: 'windtalker', + params: { + pubId: '584971', + siteId: '584971', + placementId: '123', + video: { + skipppable: true, + } + } + }] + } + ]; +``` diff --git a/test/spec/modules/windtalkerBidAdapter_spec.js b/test/spec/modules/windtalkerBidAdapter_spec.js new file mode 100644 index 00000000000..222a7611b01 --- /dev/null +++ b/test/spec/modules/windtalkerBidAdapter_spec.js @@ -0,0 +1,348 @@ +import {expect} from 'chai'; +import {spec} from 'modules/windtalkerBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; + +describe('Windtalker Adapter Tests', function () { + const slotConfigs = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bidId: 'bid12345', + mediaType: 'banner', + params: { + pubId: '29521', + siteId: '26047', + placementId: '123', + bidFloor: '0.001', + ifa: 'IFA', + latitude: '40.712775', + longitude: '-74.005973' + } + }, { + placementCode: '/DfpAccount2/slot2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bidId: 'bid23456', + mediaType: 'banner', + params: { + pubId: '29521', + siteId: '26047', + placementId: '1234', + bidFloor: '0.000001', + } + }]; + const nativeSlotConfig = [{ + placementCode: '/DfpAccount1/slot3', + bidId: 'bid12345', + mediaType: 'native', + nativeParams: { + title: { required: true, len: 200 }, + body: {}, + image: { wmin: 100 }, + sponsoredBy: { }, + icon: { } + }, + params: { + pubId: '29521', + placementId: '123', + siteId: '26047' + } + }]; + const videoSlotConfig = [{ + placementCode: '/DfpAccount1/slot4', + mediaTypes: { + video: { + playerSize: [[640, 480]] + } + }, + bidId: 'bid12345678', + mediaType: 'video', + video: { + skippable: true + }, + params: { + pubId: '29521', + placementId: '1234567', + siteId: '26047', + } + }]; + const appSlotConfig = [{ + placementCode: '/DfpAccount1/slot5', + bidId: 'bid12345', + params: { + pubId: '29521', + placementId: '1234', + app: { + id: '1111', + name: 'app name', + bundle: 'io.windtalker.apps', + storeUrl: 'https://windtalker.io/apps', + domain: 'windtalker.io' + } + } + }]; + + it('Verify build request', function () { + const request = spec.buildRequests(slotConfigs); + expect(request.url).to.equal('https://windtalkerdisplay.hb.adp3.net/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + // site object + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site.publisher).to.not.equal(null); + expect(ortbRequest.site.publisher.id).to.equal('29521'); + expect(ortbRequest.site.ref).to.equal(window.top.document.referrer); + expect(ortbRequest.site.page).to.equal(window.location.href); + expect(ortbRequest.imp).to.have.lengthOf(2); + // device object + expect(ortbRequest.device).to.not.equal(null); + expect(ortbRequest.device.ua).to.equal(navigator.userAgent); + expect(ortbRequest.device.ifa).to.equal('IFA'); + expect(ortbRequest.device.geo.lat).to.equal('40.712775'); + expect(ortbRequest.device.geo.lon).to.equal('-74.005973'); + // slot 1 + expect(ortbRequest.imp[0].tagid).to.equal('123'); + expect(ortbRequest.imp[0].banner).to.not.equal(null); + expect(ortbRequest.imp[0].banner.w).to.equal(300); + expect(ortbRequest.imp[0].banner.h).to.equal(250); + expect(ortbRequest.imp[0].bidfloor).to.equal('0.001'); + // slot 2 + expect(ortbRequest.imp[1].tagid).to.equal('1234'); + expect(ortbRequest.imp[1].banner).to.not.equal(null); + expect(ortbRequest.imp[1].banner.w).to.equal(728); + expect(ortbRequest.imp[1].banner.h).to.equal(90); + expect(ortbRequest.imp[1].bidfloor).to.equal('0.000001'); + }); + + it('Verify parse response', function () { + const request = spec.buildRequests(slotConfigs); + const ortbRequest = JSON.parse(request.data); + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + w: 300, + h: 250 + }] + }], + cur: 'USD' + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.ad).to.equal('This is an Ad'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.adId).to.equal('bid12345'); + expect(bid.creativeId).to.equal('bid12345'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(360); + }); + + it('Verify full passback', function () { + const request = spec.buildRequests(slotConfigs); + const bids = spec.interpretResponse({ body: null }, request) + expect(bids).to.have.lengthOf(0); + }); + + it('Verify Native request', function () { + const request = spec.buildRequests(nativeSlotConfig); + expect(request.url).to.equal('https://windtalkerdisplay.hb.adp3.net/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + // native impression + expect(ortbRequest.imp[0].tagid).to.equal('123'); + const nativePart = ortbRequest.imp[0]['native']; + expect(nativePart).to.not.equal(null); + expect(nativePart.ver).to.equal('1.1'); + expect(nativePart.request).to.not.equal(null); + // native request assets + const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); + expect(nativeRequest).to.not.equal(null); + expect(nativeRequest.assets).to.have.lengthOf(5); + expect(nativeRequest.assets[0].id).to.equal(1); + expect(nativeRequest.assets[1].id).to.equal(2); + expect(nativeRequest.assets[2].id).to.equal(3); + expect(nativeRequest.assets[3].id).to.equal(4); + expect(nativeRequest.assets[4].id).to.equal(5); + expect(nativeRequest.assets[0].required).to.equal(1); + expect(nativeRequest.assets[0].title).to.not.equal(null); + expect(nativeRequest.assets[0].title.len).to.equal(200); + expect(nativeRequest.assets[1].title).to.be.undefined; + expect(nativeRequest.assets[1].data).to.not.equal(null); + expect(nativeRequest.assets[1].data.type).to.equal(2); + expect(nativeRequest.assets[1].data.len).to.equal(200); + expect(nativeRequest.assets[2].required).to.equal(0); + expect(nativeRequest.assets[3].img).to.not.equal(null); + expect(nativeRequest.assets[3].img.wmin).to.equal(50); + expect(nativeRequest.assets[3].img.hmin).to.equal(50); + expect(nativeRequest.assets[3].img.type).to.equal(1); + expect(nativeRequest.assets[4].img).to.not.equal(null); + expect(nativeRequest.assets[4].img.wmin).to.equal(100); + expect(nativeRequest.assets[4].img.hmin).to.equal(150); + expect(nativeRequest.assets[4].img.type).to.equal(3); + }); + + it('Verify Native response', function () { + const request = spec.buildRequests(nativeSlotConfig); + expect(request.url).to.equal('https://windtalkerdisplay.hb.adp3.net/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + const nativeResponse = { + 'native': { + assets: [ + { id: 1, title: { text: 'Ad Title' } }, + { id: 2, data: { value: 'Test description' } }, + { id: 3, data: { value: 'Brand' } }, + { id: 4, img: { url: 'https://adx1public.s3.amazonaws.com/creatives_icon.png', w: 100, h: 100 } }, + { id: 5, img: { url: 'https://adx1public.s3.amazonaws.com/creatives_image.png', w: 300, h: 300 } } + ], + link: { url: 'https://brand.com/' } + } + }; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + nurl: 'https://rtb.adx1.com/log', + adm: JSON.stringify(nativeResponse) + }] + }], + cur: 'USD', + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + // verify bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.adId).to.equal('bid12345'); + expect(bid.ad).to.be.undefined; + expect(bid.mediaType).to.equal('native'); + const nativeBid = bid['native']; + expect(nativeBid).to.not.equal(null); + expect(nativeBid.title).to.equal('Ad Title'); + expect(nativeBid.sponsoredBy).to.equal('Brand'); + expect(nativeBid.icon.url).to.equal('https://adx1public.s3.amazonaws.com/creatives_icon.png'); + expect(nativeBid.image.url).to.equal('https://adx1public.s3.amazonaws.com/creatives_image.png'); + expect(nativeBid.image.width).to.equal(300); + expect(nativeBid.image.height).to.equal(300); + expect(nativeBid.icon.width).to.equal(100); + expect(nativeBid.icon.height).to.equal(100); + expect(nativeBid.clickUrl).to.equal(encodeURIComponent('https://brand.com/')); + expect(nativeBid.impressionTrackers).to.have.lengthOf(1); + expect(nativeBid.impressionTrackers[0]).to.equal('https://rtb.adx1.com/log'); + }); + + it('Verify Video request', function () { + const request = spec.buildRequests(videoSlotConfig); + expect(request.url).to.equal('https://windtalkerdisplay.hb.adp3.net/'); + expect(request.method).to.equal('POST'); + const videoRequest = JSON.parse(request.data); + // site object + expect(videoRequest.site).to.not.equal(null); + expect(videoRequest.site.publisher.id).to.equal('29521'); + expect(videoRequest.site.ref).to.equal(window.top.document.referrer); + expect(videoRequest.site.page).to.equal(window.location.href); + // device object + expect(videoRequest.device).to.not.equal(null); + expect(videoRequest.device.ua).to.equal(navigator.userAgent); + // slot 1 + expect(videoRequest.imp[0].tagid).to.equal('1234567'); + expect(videoRequest.imp[0].video).to.not.equal(null); + expect(videoRequest.imp[0].video.w).to.equal(640); + expect(videoRequest.imp[0].video.h).to.equal(480); + expect(videoRequest.imp[0].banner).to.equal(null); + expect(videoRequest.imp[0].native).to.equal(null); + }); + + it('Verify parse video response', function () { + const request = spec.buildRequests(videoSlotConfig); + const videoRequest = JSON.parse(request.data); + const videoResponse = { + seatbid: [{ + bid: [{ + impid: videoRequest.imp[0].id, + price: 1.90, + adm: 'https://vid.example.com/9876', + crid: '510511_754567308' + }] + }], + cur: 'USD' + }; + const bids = spec.interpretResponse({ body: videoResponse }, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.90); + expect(bid.vastUrl).to.equal('https://vid.example.com/9876'); + expect(bid.crid).to.equal('510511_754567308'); + expect(bid.width).to.equal(640); + expect(bid.height).to.equal(480); + expect(bid.adId).to.equal('bid12345678'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(360); + }); + + it('Verifies bidder code', function () { + expect(spec.code).to.equal('windtalker'); + }); + + it('Verifies supported media types', function () { + expect(spec.supportedMediaTypes).to.have.lengthOf(3); + expect(spec.supportedMediaTypes[0]).to.equal('banner'); + expect(spec.supportedMediaTypes[1]).to.equal('native'); + expect(spec.supportedMediaTypes[2]).to.equal('video'); + }); + + it('Verifies if bid request valid', function () { + expect(spec.isBidRequestValid(slotConfigs[0])).to.equal(true); + expect(spec.isBidRequestValid(slotConfigs[1])).to.equal(true); + expect(spec.isBidRequestValid(nativeSlotConfig[0])).to.equal(true); + expect(spec.isBidRequestValid(videoSlotConfig[0])).to.equal(true); + }); + + it('Verify app requests', function () { + const request = spec.buildRequests(appSlotConfig); + const ortbRequest = JSON.parse(request.data); + expect(ortbRequest.site).to.equal(null); + expect(ortbRequest.app).to.not.be.null; + expect(ortbRequest.app.publisher).to.not.equal(null); + expect(ortbRequest.app.publisher.id).to.equal('29521'); + expect(ortbRequest.app.id).to.equal('1111'); + expect(ortbRequest.app.name).to.equal('app name'); + expect(ortbRequest.app.bundle).to.equal('io.windtalker.apps'); + expect(ortbRequest.app.storeurl).to.equal('https://windtalker.io/apps'); + expect(ortbRequest.app.domain).to.equal('windtalker.io'); + }); + + it('Verify GDPR', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'serialized_gpdr_data' + } + }; + const request = spec.buildRequests(slotConfigs, bidderRequest); + expect(request.url).to.equal('https://windtalkerdisplay.hb.adp3.net/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + expect(ortbRequest.user).to.not.equal(null); + expect(ortbRequest.user.ext).to.not.equal(null); + expect(ortbRequest.user.ext.consent).to.equal('serialized_gpdr_data'); + expect(ortbRequest.regs).to.not.equal(null); + expect(ortbRequest.regs.ext).to.not.equal(null); + expect(ortbRequest.regs.ext.gdpr).to.equal(1); + }); +}); From 8275fc1cdb067aea051870426947ad03a9b2afa6 Mon Sep 17 00:00:00 2001 From: Chris Cole Date: Tue, 7 Apr 2020 12:59:38 -0700 Subject: [PATCH 73/90] #5011 Fix to set Secure attribute on cookie when SameSite=none (#5064) * #5011 Fix to set Secure attribute on cookie when SameSite=none * Minor change to use const instead of var per review request. --- src/storageManager.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/storageManager.js b/src/storageManager.js index abda0ccb180..75ad10908dc 100644 --- a/src/storageManager.js +++ b/src/storageManager.js @@ -64,7 +64,9 @@ export function newStorageManager({gvlid, moduleName, moduleType} = {}) { if (result && result.valid) { const domainPortion = (domain && domain !== '') ? ` ;domain=${encodeURIComponent(domain)}` : ''; const expiresPortion = (expires && expires !== '') ? ` ;expires=${expires}` : ''; - document.cookie = `${key}=${encodeURIComponent(value)}${expiresPortion}; path=/${domainPortion}${sameSite ? `; SameSite=${sameSite}` : ''}`; + const isNone = (sameSite != null && sameSite.toLowerCase() == 'none') + const secure = (isNone) ? '; Secure' : ''; + document.cookie = `${key}=${encodeURIComponent(value)}${expiresPortion}; path=/${domainPortion}${sameSite ? `; SameSite=${sameSite}` : ''}${secure}`; } } if (done && typeof done === 'function') { From 8d3dc0820d5f313cbd86e7ee79d732a03e8b64f3 Mon Sep 17 00:00:00 2001 From: Jason Snellbaker Date: Tue, 7 Apr 2020 16:03:05 -0400 Subject: [PATCH 74/90] Prebid 3.15.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7baae6a5f00..5c4f654c20b 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.15.0-pre", + "version": "3.15.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 78602ffd0943b85e8285d3aa116bd131afa6d1a5 Mon Sep 17 00:00:00 2001 From: Jason Snellbaker Date: Tue, 7 Apr 2020 16:13:18 -0400 Subject: [PATCH 75/90] increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c4f654c20b..66cfd10b357 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.15.0", + "version": "3.16.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 75f13a0c49d48527d6b463346428bcc729dd61e2 Mon Sep 17 00:00:00 2001 From: DeepthiNeeladri Date: Wed, 8 Apr 2020 12:26:51 +0530 Subject: [PATCH 76/90] Support min &max duration for onevideo (#5079) * outstream changes * removing global filtet * reverting page * message * adapter change * remove space * testcases * testpage * spaces for test page * renderer exist case * reverting package-lock.json * adding schain object * adding tagid * syntaxx error fix * video.html * space trailing * space * tagid * inventoryId and placement * rewarded video * added unit test case * comment * changes * min max dduration * .md file * min max * md change * version * tests fix Co-authored-by: Deepthi Neeladri Sravana Co-authored-by: Deepthi Neeladri Sravana Co-authored-by: Deepthi Neeladri Sravana Co-authored-by: Deepthi Neeladri Sravana Co-authored-by: Deepthi Neeladri Sravana Co-authored-by: Deepthi Neeladri Sravana Co-authored-by: Deepthi Neeladri Sravana Co-authored-by: Deepthi Neeladri Sravana Co-authored-by: Deepthi Neeladri Sravana Co-authored-by: Deepthi Neeladri Sravana Co-authored-by: Deepthi Neeladri Sravana --- modules/oneVideoBidAdapter.js | 11 +++++++++-- modules/oneVideoBidAdapter.md | 4 +++- test/spec/modules/oneVideoBidAdapter_spec.js | 14 +++++++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/modules/oneVideoBidAdapter.js b/modules/oneVideoBidAdapter.js index 933f2d7a4c8..5747c5e848d 100644 --- a/modules/oneVideoBidAdapter.js +++ b/modules/oneVideoBidAdapter.js @@ -3,8 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'oneVideo'; export const spec = { code: 'oneVideo', - gvlid: 25, - VERSION: '3.0.0', + VERSION: '3.0.1', ENDPOINT: 'https://ads.adaptv.advertising.com/rtb/openrtb?ext_id=', SYNC_ENDPOINT1: 'https://cm.g.doubleclick.net/pixel?google_nid=adaptv_dbm&google_cm&google_sc', SYNC_ENDPOINT2: 'https://pr-bh.ybp.yahoo.com/sync/adaptv_ortb/{combo_uid}', @@ -215,6 +214,14 @@ function getRequestData(bid, consentData, bidRequest) { if (bid.params.video.placement) { bidData.imp[0].banner.placement = bid.params.video.placement } + if (bid.params.video.maxduration) { + bidData.imp[0].banner.ext = bidData.imp[0].banner.ext || {} + bidData.imp[0].banner.ext.maxduration = bid.params.video.maxduration + } + if (bid.params.video.minduration) { + bidData.imp[0].banner.ext = bidData.imp[0].banner.ext || {} + bidData.imp[0].banner.ext.minduration = bid.params.video.minduration + } } if (bid.params.video.inventoryid) { bidData.imp[0].ext.inventoryid = bid.params.video.inventoryid diff --git a/modules/oneVideoBidAdapter.md b/modules/oneVideoBidAdapter.md index 145af1a6fb9..656f81733a0 100644 --- a/modules/oneVideoBidAdapter.md +++ b/modules/oneVideoBidAdapter.md @@ -37,7 +37,9 @@ Connects to One Video demand source to fetch bids. sid: , rewarded: 1, placement: 1, - inventoryid: 123 + inventoryid: 123, + minduration: 10, + maxduration: 30, }, site: { id: 1, diff --git a/test/spec/modules/oneVideoBidAdapter_spec.js b/test/spec/modules/oneVideoBidAdapter_spec.js index fc7c29c7b51..82546f2e96d 100644 --- a/test/spec/modules/oneVideoBidAdapter_spec.js +++ b/test/spec/modules/oneVideoBidAdapter_spec.js @@ -136,7 +136,7 @@ describe('OneVideoBidAdapter', function () { const placement = bidRequest.params.video.placement; const rewarded = bidRequest.params.video.rewarded; const inventoryid = bidRequest.params.video.inventoryid; - const VERSION = '3.0.0'; + const VERSION = '3.0.1'; expect(data.imp[0].video.w).to.equal(width); expect(data.imp[0].video.h).to.equal(height); expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); @@ -292,7 +292,9 @@ describe('OneVideoBidAdapter', function () { placement: 1, inventoryid: 123, sid: 134, - display: 1 + display: 1, + minduration: 10, + maxduration: 30 }, site: { id: 1, @@ -314,6 +316,8 @@ describe('OneVideoBidAdapter', function () { expect(data.imp[0].ext.inventoryid).to.equal(bidRequest.params.video.inventoryid); expect(data.imp[0].banner.mimes).to.equal(bidRequest.params.video.mimes); expect(data.imp[0].banner.placement).to.equal(bidRequest.params.video.placement); + expect(data.imp[0].banner.ext.minduration).to.equal(bidRequest.params.video.minduration); + expect(data.imp[0].banner.ext.maxduration).to.equal(bidRequest.params.video.maxduration); expect(data.site.id).to.equal(bidRequest.params.site.id); }); it('should send video object when display is other than 1', function () { @@ -384,7 +388,9 @@ describe('OneVideoBidAdapter', function () { delivery: [2], playbackmethod: [1, 5], placement: 123, - sid: 134 + sid: 134, + minduration: 10, + maxduration: 30 }, site: { id: 1, @@ -406,6 +412,8 @@ describe('OneVideoBidAdapter', function () { expect(data.imp[0].video.mimes).to.equal(bidRequest.params.video.mimes); expect(data.imp[0].video.protocols).to.equal(bidRequest.params.video.protocols); expect(data.imp[0].video.linearity).to.equal(1); + expect(data.imp[0].video.maxduration).to.equal(bidRequest.params.video.maxduration); + expect(data.imp[0].video.minduration).to.equal(bidRequest.params.video.minduration); }); describe('getUserSyncs', function () { const GDPR_CONSENT_STRING = 'GDPR_CONSENT_STRING'; From c61a2b941a99e91a51dd10c43fb19e874c6fff24 Mon Sep 17 00:00:00 2001 From: bretg Date: Wed, 8 Apr 2020 03:27:45 -0400 Subject: [PATCH 77/90] isSafariBrowser update (#5077) * isSafariBrowser-update * can't restore navigator object * update tests to reset navigator.userAgent value to initial value after each test that modifies the value * fix stubs the navigator userAgent property using sinon Co-authored-by: idettman --- src/utils.js | 2 +- test/spec/utils_spec.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index f43097e74b9..45822dd7088 100644 --- a/src/utils.js +++ b/src/utils.js @@ -817,7 +817,7 @@ export function inIframe() { } export function isSafariBrowser() { - return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); } export function replaceAuctionPrice(str, cpm) { diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index e7be6880444..9ba70810a08 100755 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -922,4 +922,40 @@ describe('Utils', function () { }); }); }); + + describe('isSafariBrowser', function () { + let userAgentStub; + let userAgent; + + before(function () { + userAgentStub = sinon.stub(navigator, 'userAgent').get(function () { + return userAgent; + }); + }); + + after(function () { + userAgentStub.restore(); + }); + + it('properly detects safari', function () { + userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25'; + expect(utils.isSafariBrowser()).to.equal(true); + }); + it('does not flag Chrome on MacOS', function () { + userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36'; + expect(utils.isSafariBrowser()).to.equal(false); + }); + it('does not flag Chrome iOS', function () { + userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/80.0.3987.95 Mobile/15E148 Safari/604.1'; + expect(utils.isSafariBrowser()).to.equal(false); + }); + it('does not flag Firefox iOS', function () { + userAgent = 'Mozilla/5.0 (iPhone; CPU OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/23.0 Mobile/15E148 Safari/605.1.15'; + expect(utils.isSafariBrowser()).to.equal(false); + }); + it('does not flag Windows Edge', function () { + userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.74 Safari/537.36 Edg/79.0.309.43'; + expect(utils.isSafariBrowser()).to.equal(false); + }); + }); }); From 925ae0a418890b1a845b9785b4410f52cb22e87e Mon Sep 17 00:00:00 2001 From: mglowackiym <62994965+mglowackiym@users.noreply.github.com> Date: Wed, 8 Apr 2020 03:43:59 -0400 Subject: [PATCH 78/90] CRITEO RTUS Integration with Yieldmo Prebid (#5075) * update maintainer email * add stringified schain * Revert "add stringified schain" This reverts commit 4fd8634f12133945ac44e9b550237321e71fc549. * pass along gdpr consent string and gdpr applies through get * use paramter gdprConsent * add stringified schain * don't change other files * change parameter namves * updates bidder module to be 3.0 compliant, and tests * deletes openxoutstream adapter * fix broken yieldmo test * remove log * remove utils functions that will be depricated in 3.0 and use new referer object in bidderRequest * use prebids gettopwindow util functions. update markdown with 3.0 compliant ad units object * Pass along us_privacy string * remove blank.min * Fix unit tests and remove unused variables and methods * add stringified schain * add stringified schain * Revert "add stringified schain" This reverts commit 4fd8634f12133945ac44e9b550237321e71fc549. * pass along gdpr consent string and gdpr applies through get * use paramter gdprConsent * don't change other files * change parameter namves * updates bidder module to be 3.0 compliant, and tests * deletes openxoutstream adapter * fix broken yieldmo test * remove log * remove utils functions that will be depricated in 3.0 and use new referer object in bidderRequest * use prebids gettopwindow util functions. update markdown with 3.0 compliant ad units object * Pass along us_privacy string * update maintainer email * add stringified schain * Revert "add stringified schain" This reverts commit 4fd8634f12133945ac44e9b550237321e71fc549. * add stringified schain * don't change other files * change parameter namves * updates bidder module to be 3.0 compliant, and tests * fix broken yieldmo test * remove utils functions that will be depricated in 3.0 and use new referer object in bidderRequest * use prebids gettopwindow util functions. update markdown with 3.0 compliant ad units object * Pass along us_privacy string * Clean up console * Do not undo my featureeee * CRITEO RTUS For prebid * Tests for CRITEO RTUS For prebid * Revert "Do not undo my featureeee" This reverts commit 0185407646a6ba15a9d99ab19cc1ea0f3b6bdadd. * linting * add test back in: Co-authored-by: Melody Li Co-authored-by: Elber Carneiro Co-authored-by: Elber Carneiro Co-authored-by: Melody Li Co-authored-by: Melody Li Co-authored-by: Andrew Holz Co-authored-by: Michal --- modules/yieldmoBidAdapter.js | 114 +++++++----- modules/yieldmoBidAdapter.md | 1 + test/spec/modules/yieldmoBidAdapter_spec.js | 188 +++++++++++++------- 3 files changed, 193 insertions(+), 110 deletions(-) diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 7a33dd0db91..5ef146cb2d0 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -13,10 +13,10 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: ['banner'], /** - * Determines whether or not the given bid request is valid. - * @param {object} bid, bid to validate - * @return boolean, true if valid, otherwise false - */ + * Determines whether or not the given bid request is valid. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ isBidRequestValid: function(bid) { return !!(bid && bid.adUnitCode && bid.bidId); }, @@ -41,13 +41,19 @@ export const spec = { h: localWindow.innerHeight, userConsent: JSON.stringify({ // case of undefined, stringify will remove param - gdprApplies: bidderRequest && bidderRequest.gdprConsent ? bidderRequest.gdprConsent.gdprApplies : undefined, - cmp: bidderRequest && bidderRequest.gdprConsent ? bidderRequest.gdprConsent.consentString : undefined + gdprApplies: + bidderRequest && bidderRequest.gdprConsent + ? bidderRequest.gdprConsent.gdprApplies + : undefined, + cmp: + bidderRequest && bidderRequest.gdprConsent + ? bidderRequest.gdprConsent.consentString + : undefined }), - us_privacy: bidderRequest && bidderRequest.us_privacy, + us_privacy: bidderRequest && bidderRequest.us_privacy }; - bidRequests.forEach((request) => { + bidRequests.forEach(request => { serverRequest.p.push(addPlacement(request)); const pubcid = getId(request, 'pubcid'); if (pubcid) { @@ -61,6 +67,10 @@ export const spec = { if (tdid) { serverRequest.tdid = tdid; } + const criteoId = getId(request, 'criteoId'); + if (criteoId) { + serverRequest.cri_prebid = criteoId; + } if (request.schain) { serverRequest.schain = JSON.stringify(request.schain); } @@ -70,7 +80,7 @@ export const spec = { method: 'GET', url: SERVER_ENDPOINT, data: serverRequest - } + }; }, /** * Makes Yieldmo Ad Server response compatible to Prebid specs @@ -81,7 +91,7 @@ export const spec = { let bids = []; let data = serverResponse.body; if (data.length > 0) { - data.forEach((response) => { + data.forEach(response => { if (response.cpm && response.cpm > 0) { bids.push(createNewBid(response)); } @@ -91,15 +101,17 @@ export const spec = { }, getUserSync: function(syncOptions) { if (trackingEnabled(syncOptions)) { - return [{ - type: 'iframe', - url: SYNC_ENDPOINT + utils.getOrigin() - }]; + return [ + { + type: 'iframe', + url: SYNC_ENDPOINT + utils.getOrigin() + } + ]; } else { return []; } } -} +}; registerBidder(spec); /*************************************** @@ -115,7 +127,7 @@ function addPlacement(request) { placement_id: request.adUnitCode, callback_id: request.bidId, sizes: request.mediaTypes.banner.sizes - } + }; if (request.params) { if (request.params.placementId) { placementInfo.ym_placement_id = request.params.placementId; @@ -128,9 +140,9 @@ function addPlacement(request) { } /** - * creates a new bid with response information - * @param response server response - */ + * creates a new bid with response information + * @param response server response + */ function createNewBid(response) { return { requestId: response['callback_id'], @@ -150,23 +162,25 @@ function createNewBid(response) { * @returns false if dnt or if not iframe/pixel enabled */ function trackingEnabled(options) { - return (isIOS() && !getDNT() && options.iframeEnabled); + return isIOS() && !getDNT() && options.iframeEnabled; } /** - * Detects whether we're in iOS - * @returns true if in iOS - */ + * Detects whether we're in iOS + * @returns true if in iOS + */ function isIOS() { return /iPhone|iPad|iPod/i.test(window.navigator.userAgent); } /** - * Detects whether dnt is true - * @returns true if user enabled dnt - */ + * Detects whether dnt is true + * @returns true if user enabled dnt + */ function getDNT() { - return window.doNotTrack === '1' || window.navigator.doNotTrack === '1' || false; + return ( + window.doNotTrack === '1' || window.navigator.doNotTrack === '1' || false + ); } /** @@ -174,7 +188,9 @@ function getDNT() { */ function getPageDescription() { if (document.querySelector('meta[name="description"]')) { - return document.querySelector('meta[name="description"]').getAttribute('content'); // Value of the description metadata from the publisher's page. + return document + .querySelector('meta[name="description"]') + .getAttribute('content'); // Value of the description metadata from the publisher's page. } else { return ''; } @@ -200,9 +216,9 @@ function getPageDescription() { */ /** - * Detects what environment we're in - * @returns Environment kind - */ + * Detects what environment we're in + * @returns Environment kind + */ function getEnvironment() { if (isSuperSandboxedIframe()) { return 89; @@ -228,28 +244,31 @@ function getEnvironment() { } /** - * @returns true if we are running on the top window at dispatch time - */ + * @returns true if we are running on the top window at dispatch time + */ function isCodeOnPage() { return window === window.parent; } /** - * @returns true if the environment is both DFP and AMP - */ + * @returns true if the environment is both DFP and AMP + */ function isDfpInAmp() { return isDfp() && isAmp(); } /** - * @returns true if the window is in an iframe whose id and parent element id match DFP - */ + * @returns true if the window is in an iframe whose id and parent element id match DFP + */ function isDfp() { try { const frameElement = window.frameElement; const parentElement = window.frameElement.parentNode; if (frameElement && parentElement) { - return frameElement.id.indexOf('google_ads_iframe') > -1 && parentElement.id.indexOf('google_ads_iframe') > -1; + return ( + frameElement.id.indexOf('google_ads_iframe') > -1 && + parentElement.id.indexOf('google_ads_iframe') > -1 + ); } return false; } catch (e) { @@ -258,8 +277,8 @@ function isDfp() { } /** -* @returns true if there is an AMP context object -*/ + * @returns true if there is an AMP context object + */ function isAmp() { try { const ampContext = window.context || window.parent.context; @@ -285,7 +304,11 @@ function isSafeFrame() { function isDFPSafeFrame() { if (window.location && window.location.href) { const href = window.location.href; - return isSafeFrame() && href.indexOf('google') !== -1 && href.indexOf('safeframe') !== -1; + return ( + isSafeFrame() && + href.indexOf('google') !== -1 && + href.indexOf('safeframe') !== -1 + ); } return false; } @@ -318,7 +341,7 @@ function isSuperSandboxedIframe() { * @returns true if the window has the attribute identifying MRAID */ function isMraid() { - return !!(window.mraid); + return !!window.mraid; } /** @@ -329,7 +352,12 @@ function isMraid() { */ function getId(request, idType) { let id; - if (request && request.userId && request.userId[idType] && typeof request.userId === 'object') { + if ( + request && + request.userId && + request.userId[idType] && + typeof request.userId === 'object' + ) { id = request.userId[idType]; } return id; diff --git a/modules/yieldmoBidAdapter.md b/modules/yieldmoBidAdapter.md index ef5f07a4eb9..0f86d2507d1 100644 --- a/modules/yieldmoBidAdapter.md +++ b/modules/yieldmoBidAdapter.md @@ -4,6 +4,7 @@ Module Name: Yieldmo Bid Adapter Module Type: Bidder Adapter Maintainer: opensource@yieldmo.com +Note: Our ads will only render in mobile ``` # Description diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 0bbc4b57785..5904113bd42 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -8,44 +8,48 @@ describe('YieldmoAdapter', function () { const ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; let tdid = '8d146286-91d4-4958-aff4-7e489dd1abd6'; + let criteoId = 'aff4'; let bid = { bidder: 'yieldmo', params: { - bidFloor: 0.1 + bidFloor: 0.1, }, adUnitCode: 'adunit-code', mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]] - } + sizes: [ + [300, 250], + [300, 600], + ], + }, }, bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', crumbs: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da' + pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da', }, userId: { tdid, - } + }, }; let bidArray = [bid]; let bidderRequest = { - 'bidderCode': 'yieldmo', - 'auctionId': 'e3a336ad-2761-4a1c-b421-ecc7c5294a34', - 'bidderRequestId': '14c4ede8c693f', - 'bids': bidArray, - 'auctionStart': 1520001292880, - 'timeout': 3000, - 'start': 1520001292884, - 'doneCbCallCount': 0, - 'refererInfo': { - 'numIframes': 1, - 'reachedTop': true, - 'referer': 'yieldmo.com' - } - } + bidderCode: 'yieldmo', + auctionId: 'e3a336ad-2761-4a1c-b421-ecc7c5294a34', + bidderRequestId: '14c4ede8c693f', + bids: bidArray, + auctionStart: 1520001292880, + timeout: 3000, + start: 1520001292884, + doneCbCallCount: 0, + refererInfo: { + numIframes: 1, + reachedTop: true, + referer: 'yieldmo.com', + }, + }; describe('isBidRequestValid', function () { it('should return true when necessary information is found', function () { @@ -77,38 +81,44 @@ describe('YieldmoAdapter', function () { }); it('should not blow up if crumbs is undefined', function () { - let bidArray = [ - { ...bid, crumbs: undefined } - ] - expect(function () { spec.buildRequests(bidArray, bidderRequest) }).not.to.throw() - }) + let bidArray = [{ ...bid, crumbs: undefined }]; + expect(function () { + spec.buildRequests(bidArray, bidderRequest); + }).not.to.throw(); + }); it('should place bid information into the p parameter of data', function () { let placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.equal('[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1}]'); + expect(placementInfo).to.equal( + '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1}]' + ); bidArray.push({ bidder: 'yieldmo', params: { - bidFloor: 0.2 + bidFloor: 0.2, }, adUnitCode: 'adunit-code-1', mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]] - } + sizes: [ + [300, 250], + [300, 600], + ], + }, }, bidId: '123456789', bidderRequestId: '987654321', auctionId: '0246810', crumbs: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da' - } - + pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da', + }, }); // multiple placements placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.equal('[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1},{"placement_id":"adunit-code-1","callback_id":"123456789","sizes":[[300,250],[300,600]],"bidFloor":0.2}]'); + expect(placementInfo).to.equal( + '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1},{"placement_id":"adunit-code-1","callback_id":"123456789","sizes":[[300,250],[300,600]],"bidFloor":0.2}]' + ); }); it('should add placement id if given', function () { @@ -145,18 +155,23 @@ describe('YieldmoAdapter', function () { adUnitCode: 'adunit-code', mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]] - } + sizes: [ + [300, 250], + [300, 600], + ], + }, }, bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', userId: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2' - } + pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2', + }, }; const data = spec.buildRequests([pubcidBid], bidderRequest).data; - expect(data.pubcid).to.deep.equal('c604130c-0144-4b63-9bf2-c2bd8c8d86da2'); + expect(data.pubcid).to.deep.equal( + 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2' + ); }); it('should add unified id as parameter of request', function () { @@ -166,31 +181,60 @@ describe('YieldmoAdapter', function () { adUnitCode: 'adunit-code', mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]] - } + sizes: [ + [300, 250], + [300, 600], + ], + }, }, bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', userId: { tdid, - } + }, }; const data = spec.buildRequests([unifiedIdBid], bidderRequest).data; expect(data.tdid).to.deep.equal(tdid); }); + it('should add CRITEO RTUS id as parameter of request', function () { + const criteoIdBid = { + bidder: 'yieldmo', + params: {}, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: { + criteoId, + }, + }; + const data = spec.buildRequests([criteoIdBid], bidderRequest).data; + expect(data.cri_prebid).to.deep.equal(criteoId); + }); + it('should add gdpr information to request if available', () => { bidderRequest.gdprConsent = { - 'consentString': 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', - 'vendorData': {'blerp': 1}, - 'gdprApplies': true - } + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: { blerp: 1 }, + gdprApplies: true, + }; const data = spec.buildRequests(bidArray, bidderRequest).data; - expect(data.userConsent).equal(JSON.stringify({ - 'gdprApplies': true, - 'cmp': 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==' - })); + expect(data.userConsent).equal( + JSON.stringify({ + gdprApplies: true, + cmp: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + }) + ); }); it('should add ccpa information to request if available', () => { @@ -201,11 +245,15 @@ describe('YieldmoAdapter', function () { }); it('should add schain if it is in the bidRequest', () => { - const schain = {'ver': '1.0', 'complete': 1, 'nodes': [{'asi': 'indirectseller.com', 'sid': '00001', 'hp': 1}]}; + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; bidArray[0].schain = schain; const request = spec.buildRequests([bidArray[0]], bidderRequest); expect(request.data.schain).equal(JSON.stringify(schain)); - }) + }); }); describe('interpretResponse', function () { @@ -213,17 +261,20 @@ describe('YieldmoAdapter', function () { beforeEach(function () { serverResponse = { - body: [{ - callback_id: '21989fdbef550a', - cpm: 3.45455, - width: 300, - height: 250, - ad: '
', - creative_id: '9874652394875' - }], - header: 'header?' + body: [ + { + callback_id: '21989fdbef550a', + cpm: 3.45455, + width: 300, + height: 250, + ad: + '
', + creative_id: '9874652394875', + }, + ], + header: 'header?', }; - }) + }); it('should correctly reorder the server response', function () { const newResponse = spec.interpretResponse(serverResponse); @@ -237,7 +288,8 @@ describe('YieldmoAdapter', function () { currency: 'USD', netRevenue: true, ttl: 300, - ad: '
' + ad: + '
', }); }); @@ -248,7 +300,7 @@ describe('YieldmoAdapter', function () { serverResponse.body[0].cpm = null; response = spec.interpretResponse(serverResponse); - expect(response).to.deep.equal([]) + expect(response).to.deep.equal([]); }); }); @@ -256,15 +308,17 @@ describe('YieldmoAdapter', function () { const SYNC_ENDPOINT = 'https://static.yieldmo.com/blank.min.html?orig='; let options = { iframeEnabled: true, - pixelEnabled: true + pixelEnabled: true, }; it('should return a tracker with type and url as parameters', function () { if (/iPhone|iPad|iPod/i.test(window.navigator.userAgent)) { - expect(spec.getUserSync(options)).to.deep.equal([{ - type: 'iframe', - url: SYNC_ENDPOINT + utils.getOrigin() - }]); + expect(spec.getUserSync(options)).to.deep.equal([ + { + type: 'iframe', + url: SYNC_ENDPOINT + utils.getOrigin(), + }, + ]); options.iframeEnabled = false; expect(spec.getUserSync(options)).to.deep.equal([]); From c318c02ab49c15acc28811caaffc9cc4c19fd63c Mon Sep 17 00:00:00 2001 From: Prebid Manager <49466873+Prebid-Manager@users.noreply.github.com> Date: Wed, 8 Apr 2020 16:12:24 +0700 Subject: [PATCH 79/90] Prebid manager analytics utm tags (#4998) * prebidmanager add utm tags support * prebidmanager add utm tags support tests * prebidmanager add utm tags support tests * prebidmanager add utm tags support fixes Co-authored-by: apuzanova --- modules/prebidmanagerAnalyticsAdapter.js | 34 ++++++++++++++++++- .../prebidmanagerAnalyticsAdapter_spec.js | 34 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/modules/prebidmanagerAnalyticsAdapter.js b/modules/prebidmanagerAnalyticsAdapter.js index b8112628cb6..b98ca864cd5 100644 --- a/modules/prebidmanagerAnalyticsAdapter.js +++ b/modules/prebidmanagerAnalyticsAdapter.js @@ -20,6 +20,7 @@ var _startAuction = 0; var _bidRequestTimeout = 0; let flushInterval; var pmAnalyticsEnabled = false; +const utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; var w = window; var d = document; @@ -68,6 +69,36 @@ prebidmanagerAnalytics.disableAnalytics = function() { prebidmanagerAnalytics.originDisableAnalytics(); }; +function collectUtmTagData() { + let newUtm = false; + let pmUtmTags = {}; + try { + utmTags.forEach(function (utmKey) { + let utmValue = utils.getParameterByName(utmKey); + if (utmValue !== '') { + newUtm = true; + } + pmUtmTags[utmKey] = utmValue; + }); + if (newUtm === false) { + utmTags.forEach(function (utmKey) { + let itemValue = localStorage.getItem(`pm_${utmKey}`); + if (itemValue.length !== 0) { + pmUtmTags[utmKey] = itemValue; + } + }); + } else { + utmTags.forEach(function (utmKey) { + localStorage.setItem(`pm_${utmKey}`, pmUtmTags[utmKey]); + }); + } + } catch (e) { + utils.logError(`${analyticsName}Error`, e); + pmUtmTags['error_utm'] = 1; + } + return pmUtmTags; +} + function flush() { if (!pmAnalyticsEnabled) { return; @@ -78,7 +109,8 @@ function flush() { pageViewId: _pageViewId, ver: _VERSION, bundleId: initOptions.bundleId, - events: _eventQueue + events: _eventQueue, + utmTags: collectUtmTagData(), }; ajax( diff --git a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js index 7c9173c14ab..e87be40314c 100644 --- a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js +++ b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js @@ -101,4 +101,38 @@ describe('Prebid Manager Analytics Adapter', function () { sinon.assert.callCount(prebidmanagerAnalytics.track, 6); }); }); + + describe('build utm tag data', function () { + beforeEach(function () { + localStorage.setItem('pm_utm_source', 'utm_source'); + localStorage.setItem('pm_utm_medium', 'utm_medium'); + localStorage.setItem('pm_utm_campaign', 'utm_camp'); + localStorage.setItem('pm_utm_term', ''); + localStorage.setItem('pm_utm_content', ''); + }); + afterEach(function () { + localStorage.removeItem('pm_utm_source'); + localStorage.removeItem('pm_utm_medium'); + localStorage.removeItem('pm_utm_campaign'); + localStorage.removeItem('pm_utm_term'); + localStorage.removeItem('pm_utm_content'); + prebidmanagerAnalytics.disableAnalytics() + }); + it('should build utm data from local storage', function () { + prebidmanagerAnalytics.enableAnalytics({ + provider: 'prebidmanager', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.utmTags.utm_source).to.equal('utm_source'); + expect(pmEvents.utmTags.utm_medium).to.equal('utm_medium'); + expect(pmEvents.utmTags.utm_campaign).to.equal('utm_camp'); + expect(pmEvents.utmTags.utm_term).to.equal(''); + expect(pmEvents.utmTags.utm_content).to.equal(''); + }); + }); }); From 5fab5e1cc9ddbef246f1e780e61dbaf829748059 Mon Sep 17 00:00:00 2001 From: Toshiki Yamamoto <6803209+Tosh39@users.noreply.github.com> Date: Wed, 8 Apr 2020 19:43:41 +0900 Subject: [PATCH 80/90] New bid adapter: Wipes (#5051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add wipes * done update notes * URL変更 * update test parameter * fix mediaType to make it work * fix spec error * fix comment * fix param names * update fix * erase unused code Co-authored-by: 嘉数貴明 Co-authored-by: yaki(kkz) --- modules/wipesBidAdapter.js | 69 ++++++++++ modules/wipesBidAdapter.md | 33 +++++ test/spec/modules/wipesBidAdapter_spec.js | 148 ++++++++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 modules/wipesBidAdapter.js create mode 100644 modules/wipesBidAdapter.md create mode 100644 test/spec/modules/wipesBidAdapter_spec.js diff --git a/modules/wipesBidAdapter.js b/modules/wipesBidAdapter.js new file mode 100644 index 00000000000..c60ffba0815 --- /dev/null +++ b/modules/wipesBidAdapter.js @@ -0,0 +1,69 @@ +import * as utils from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'wipes'; +const ALIAS_BIDDER_CODE = ['wi']; +const SUPPORTED_MEDIA_TYPES = [BANNER] +const ENDPOINT_URL = 'https://adn-srv.reckoner-api.com/v1/prebid'; + +function isBidRequestValid(bid) { + switch (true) { + case !!(bid.params.asid): + break; + default: + utils.logWarn(`isBidRequestValid Error. ${bid.params}, please check your implementation.`); + return false; + } + return true; +} + +function buildRequests(validBidRequests, bidderRequest) { + return validBidRequests.map(bidRequest => { + const params = bidRequest.params; + const asid = params.asid; + return { + method: 'GET', + url: ENDPOINT_URL, + data: { + asid: asid, + } + } + }); +} + +function interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; + const response = serverResponse.body; + const cpm = response.cpm * 1000 || 0; + if (cpm !== 0) { + const netRevenue = (response.netRevenue === undefined) ? true : response.netRevenue; + const bidResponse = { + requestId: response.uuid, + cpm: cpm, + width: response.width, + height: response.height, + creativeId: response.video_creative_id || 0, + dealId: response.deal_id, + currency: 'JPY', + netRevenue: netRevenue, + ttl: config.getConfig('_bidderTimeout'), + referrer: bidRequest.data.r || '', + mediaType: BANNER, + ad: response.ad_tag, + }; + bidResponses.push(bidResponse); + } + return bidResponses; +} + +export const spec = { + code: BIDDER_CODE, + aliases: ALIAS_BIDDER_CODE, + isBidRequestValid, + buildRequests, + interpretResponse, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES +} +registerBidder(spec); diff --git a/modules/wipesBidAdapter.md b/modules/wipesBidAdapter.md new file mode 100644 index 00000000000..ce7d4289079 --- /dev/null +++ b/modules/wipesBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: WIPES Bidder Adapter +Module Type: Bidder Adapter +Maintainer: contact@3-shake.com +``` + +# Description + +Connect to WIPES for bids. +Publishers needs to be set up and approved by WIPES team to enable this adapter. +Please contact support@wipestream.com for further information. + +# Test Parameters +```javascript +var adUnits = [ + // adUnit + { + code: 'video-div', + mediaTypes: { + banner: { + sizes: [[160, 300]], + } + }, + bids: [{ + bidder: 'wipes', + params: { + asid: 'dWyPondh2EGB_bNlrVjzIXRZO9F0k1dpo0I8ZvQ' + } + }] + }] +``` diff --git a/test/spec/modules/wipesBidAdapter_spec.js b/test/spec/modules/wipesBidAdapter_spec.js new file mode 100644 index 00000000000..b5935563526 --- /dev/null +++ b/test/spec/modules/wipesBidAdapter_spec.js @@ -0,0 +1,148 @@ +import {expect} from 'chai'; +import {spec} from 'modules/wipesBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; + +const ENDPOINT_URL = 'https://adn-srv.reckoner-api.com/v1/prebid'; + +describe('wipesBidAdapter', function () { + const adapter = newBidder(spec); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'wipes', + 'params': { + asid: 'dWyPondh2EGB_bNlrVjzIXRZO9F0k1dpo0I8ZvQ' + }, + 'adUnitCode': 'adunit-code', + 'bidId': '51ef8751f9aead', + 'bidderRequestId': '15246a574e859f', + 'auctionId': 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when asid not passed correctly', function () { + bid.params.asid = ''; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when require params are not passed', function () { + let bid = Object.assign({}, bid); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests = [ + { + 'bidder': 'wipes', + 'params': { + asid: 'dWyPondh2EGB_bNlrVjzIXRZO9F0k1dpo0I8ZvQ' + }, + 'adUnitCode': 'adunit-code', + 'bidId': '51ef8751f9aead', + 'bidderRequestId': '15246a574e859f', + 'auctionId': 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + }, + { + 'bidder': 'wipes', + 'params': { + asid: 'dWyPondh2EGB_bNlrVjzIXRZO9F0k1dpo0I8ZvQ' + }, + 'adUnitCode': 'adunit-code2', + 'bidId': '51ef8751f9aead', + 'bidderRequestId': '15246a574e859f', + 'auctionId': 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + } + ]; + + let bidderRequest = { + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'http://example.com', + stack: ['http://example.com'] + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + it('sends bid request to our endpoint via GET', function () { + expect(request[0].method).to.equal('GET'); + expect(request[1].method).to.equal('GET'); + }); + + it('attaches source and version to endpoint URL as query params', function () { + expect(request[0].url).to.equal(ENDPOINT_URL); + expect(request[1].url).to.equal(ENDPOINT_URL); + }); + + it('adUnitCode should be sent as uc parameters on any requests', function () { + expect(request[0].data.asid).to.equal('dWyPondh2EGB_bNlrVjzIXRZO9F0k1dpo0I8ZvQ'); + expect(request[1].data.asid).to.equal('dWyPondh2EGB_bNlrVjzIXRZO9F0k1dpo0I8ZvQ'); + }); + }); + + describe('interpretResponse', function () { + let bidRequestVideo = [ + { + 'method': 'GET', + 'url': ENDPOINT_URL, + 'data': { + 'asid': 'dWyPondh2EGB_bNlrVjzIXRZO9F0k1dpo0I8ZvQ', + } + } + ]; + + let serverResponseVideo = { + body: { + 'uuid': 'a42947f8-f8fd-4cf7-bb72-31a87ab1f6ff', + 'ad_tag': '', + 'height': 160, + 'width': 300, + 'cpm': 850, + 'status_message': '', + 'currency': 'JPY', + 'video_creative_id': 600004 + } + }; + + it('should get the correct bid response for video', function () { + let expectedResponse = [{ + 'requestId': '23beaa6af6cdde', + 'cpm': 850, + 'width': 300, + 'height': 160, + 'creativeId': '600004', + 'dealId': undefined, + 'currency': 'JPY', + 'netRevenue': true, + 'ttl': 3000, + 'referrer': '', + 'mediaType': 'banner', + 'ad': '' + }]; + let result = spec.interpretResponse(serverResponseVideo, bidRequestVideo[0]); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + expect(result[0].mediaType).to.equal(expectedResponse[0].mediaType); + }); + + it('handles empty bid response', function () { + let response = { + body: { + 'uid': 'a42947f8-f8fd-4cf7-bb72-31a87ab1f6ff', + 'height': 0, + 'crid': '', + 'statusMessage': '', + 'width': 0, + 'cpm': 0 + } + }; + let result = spec.interpretResponse(response, bidRequestVideo[0]); + expect(result.length).to.equal(0); + }); + }); +}); From 1a8406f4325cea3fd2d467e680d019e945773d49 Mon Sep 17 00:00:00 2001 From: "Takaaki.Kojima" Date: Wed, 8 Apr 2020 22:22:00 +0900 Subject: [PATCH 81/90] updateAdGenerationManual (#5032) --- modules/adgenerationBidAdapter.md | 86 ++++++++++++++++--------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/modules/adgenerationBidAdapter.md b/modules/adgenerationBidAdapter.md index 7dfc301e657..7f66732ff38 100644 --- a/modules/adgenerationBidAdapter.md +++ b/modules/adgenerationBidAdapter.md @@ -15,57 +15,61 @@ AdGeneration bid adapter supports Banner and Native. # Test Parameters ``` var adUnits = [ - // Banner adUnit - { - code: 'banner-div', // banner - sizes: [[300, 250]], - bids: [ - { - bidder: 'adg', - params: { - id: '58278', // banner - } - }, - ] - }, - // Native adUnit - { - code: 'native-div', - sizes: [[1,1]], - mediaTypes: { - native: { - image: { + // Banner adUnit + { + code: 'banner-div', // banner + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'adg', + params: { + id: '58278', // banner + } + }, + ] + }, + // Native adUnit + { + code: 'native-div', + sizes: [[1,1]], + mediaTypes: { + native: { + image: { required: true - }, - title: { + }, + title: { required: true, len: 80 - }, - sponsoredBy: { + }, + sponsoredBy: { required: true - }, - clickUrl: { + }, + clickUrl: { required: true - }, - body: { + }, + body: { required: true - }, - icon: { + }, + icon: { required: true - }, - privacyLink: { + }, + privacyLink: { required: true - }, }, - }, - bids: [ - { - bidder: 'adg', - params: { + }, + }, + bids: [ + { + bidder: 'adg', + params: { id: '58279', //native - } - }, - ] + } + }, + ] }, ]; ``` From de504a9bb6fd0f5a9954a0949f4d22837e58b7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9onard=20Labat?= Date: Wed, 8 Apr 2020 16:07:04 +0200 Subject: [PATCH 82/90] Criteo : added first party data mapping to bidder request (#4954) --- modules/criteoBidAdapter.js | 19 +++- test/spec/modules/criteoBidAdapter_spec.js | 114 ++++++++++++++++++++- 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 65675248208..7c2c5d69ad3 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -9,7 +9,7 @@ import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; import { getStorageManager } from '../src/storageManager.js'; const GVLID = 91; -export const ADAPTER_VERSION = 26; +export const ADAPTER_VERSION = 27; const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; const CRITEO_VENDOR_ID = 91; @@ -58,7 +58,11 @@ export const spec = { let url; let data; - Object.assign(bidderRequest, { ceh: config.getConfig('criteo.ceh') }); + Object.assign(bidderRequest, { + publisherExt: config.getConfig('fpd.context'), + userExt: config.getConfig('fpd.user'), + ceh: config.getConfig('criteo.ceh') + }); // If publisher tag not already loaded try to get it from fast bid if (!publisherTagAvailable()) { @@ -252,6 +256,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { const request = { publisher: { url: context.url, + ext: bidderRequest.publisherExt }, slots: bidRequests.map(bidRequest => { networkId = bidRequest.params.networkId || networkId; @@ -264,6 +269,12 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (bidRequest.params.zoneId) { slot.zoneid = bidRequest.params.zoneId; } + if (bidRequest.fpd && bidRequest.fpd.context) { + slot.ext = bidRequest.fpd.context; + } + if (bidRequest.params.ext) { + slot.ext = Object.assign({}, slot.ext, bidRequest.params.ext); + } if (bidRequest.params.publisherSubId) { slot.publishersubid = bidRequest.params.publisherSubId; } @@ -293,7 +304,9 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (networkId) { request.publisher.networkid = networkId; } - request.user = {}; + request.user = { + ext: bidderRequest.userExt + }; if (bidderRequest && bidderRequest.ceh) { request.user.ceh = bidderRequest.ceh; } diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index 9c96cfa3a05..b1319871b34 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -7,17 +7,20 @@ import { config } from '../../../src/config.js'; import { NATIVE, VIDEO } from '../../../src/mediaTypes.js'; describe('The Criteo bidding adapter', function () { - let utilsMock; + let utilsMock, sandbox; beforeEach(function () { // Remove FastBid to avoid side effects localStorage.removeItem('criteo_fast_bid'); utilsMock = sinon.mock(utils); + + sandbox = sinon.sandbox.create(); }); afterEach(function() { global.Criteo = undefined; utilsMock.restore(); + sandbox.restore(); }); describe('isBidRequestValid', function () { @@ -705,6 +708,115 @@ describe('The Criteo bidding adapter', function () { expect(request.data.user).to.not.be.null; expect(request.data.user.ceh).to.equal('hashedemail'); }); + + it('should properly build a request without first party data', function () { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + sizes: [[728, 90]], + params: { + zoneId: 123 + } + }, + ]; + + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + }; + return utils.deepAccess(config, key); + }); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.publisher.ext).to.equal(undefined); + expect(request.data.user.ext).to.equal(undefined); + expect(request.data.slots[0].ext).to.equal(undefined); + }); + + it('should properly build a request with criteo specific ad unit first party data', function () { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + sizes: [[728, 90]], + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + } + }, + ]; + + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + }; + return utils.deepAccess(config, key); + }); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.slots[0].ext).to.deep.equal({ + bidfloor: 0.75, + }); + }); + + it('should properly build a request with first party data', function () { + const contextData = { + keywords: ['power tools'], + data: { + pageType: 'article' + } + }; + const userData = { + gender: 'M', + data: { + registered: true + } + }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + sizes: [[728, 90]], + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + }, + fpd: { + context: { + data: { + someContextAttribute: 'abc' + } + } + } + }, + ]; + + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + fpd: { + context: contextData, + user: userData + } + }; + return utils.deepAccess(config, key); + }); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.publisher.ext).to.deep.equal(contextData); + expect(request.data.user.ext).to.deep.equal(userData); + expect(request.data.slots[0].ext).to.deep.equal({ + bidfloor: 0.75, + data: { + someContextAttribute: 'abc' + } + }); + }); }); describe('interpretResponse', function () { From 9eec929dfc67eeb04604987edfa1e78633f43bea Mon Sep 17 00:00:00 2001 From: bjorn-lw <32431346+bjorn-lw@users.noreply.github.com> Date: Wed, 8 Apr 2020 18:39:52 +0200 Subject: [PATCH 83/90] Read OpenRTB app objects if set in config + bug fix for when ad units are reloaded (#5086) * Livewrapped bid and analytics adapter * Fixed some tests for browser compatibility * Fixed some tests for browser compatibility * Changed analytics adapter code name * Fix double quote in debug message * modified how gdpr is being passed * Added support for Publisher Common ID Module * Corrections for ttr in analytics * ANalytics updates * Auction start time stamp changed * Detect recovered ad blocked requests Make it possible to pass dynamic parameters to adapter * Collect info on ad units receiving any valid bid * Support for ID5 Pass metadata from adapter * Typo in test + eids on wrong level * Fix for Prebid 3.0 * Fix get referer * http -> https in tests * Native support * Read sizes from mediatype.banner * Revert accidental commit * Support native data collection + minor refactorings * Set analytics endpoint * Support for app parameters * Fix issue where adunits with bids were not counted on reload --- modules/livewrappedAnalyticsAdapter.js | 36 +++--- modules/livewrappedBidAdapter.js | 51 +++++++- .../modules/livewrappedBidAdapter_spec.js | 119 +++++++++++++++--- 3 files changed, 170 insertions(+), 36 deletions(-) diff --git a/modules/livewrappedAnalyticsAdapter.js b/modules/livewrappedAnalyticsAdapter.js index 47f7f60d141..b331448161e 100644 --- a/modules/livewrappedAnalyticsAdapter.js +++ b/modules/livewrappedAnalyticsAdapter.js @@ -16,8 +16,7 @@ let initOptions; export const BID_WON_TIMEOUT = 500; const cache = { - auctions: {}, - bidAdUnits: {} + auctions: {} }; let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE}), { @@ -28,7 +27,7 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE switch (eventType) { case CONSTANTS.EVENTS.AUCTION_INIT: utils.logInfo('LIVEWRAPPED_AUCTION_INIT:', args); - cache.auctions[args.auctionId] = {bids: {}}; + cache.auctions[args.auctionId] = {bids: {}, bidAdUnits: {}}; break; case CONSTANTS.EVENTS.BID_REQUESTED: utils.logInfo('LIVEWRAPPED_BID_REQUESTED:', args); @@ -64,8 +63,12 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE if (!bidResponse.ttr) { bidResponse.ttr = time - bidResponse.start; } - if (!cache.bidAdUnits[bidResponse.adUnit]) { - cache.bidAdUnits[bidResponse.adUnit] = {sent: 0, timeStamp: cache.auctions[args.auctionId].timeStamp}; + if (!cache.auctions[args.auctionId].bidAdUnits[bidResponse.adUnit]) { + cache.auctions[args.auctionId].bidAdUnits[bidResponse.adUnit] = + { + sent: 0, + timeStamp: cache.auctions[args.auctionId].timeStamp + }; } break; case CONSTANTS.EVENTS.BIDDER_DONE: @@ -240,16 +243,19 @@ function getTimeouts() { function getbidAdUnits() { var bidAdUnits = []; - Object.keys(cache.bidAdUnits).forEach(adUnit => { - let bidAdUnit = cache.bidAdUnits[adUnit]; - if (!bidAdUnit.sent) { - bidAdUnit.sent = 1; - - bidAdUnits.push({ - adUnit: adUnit, - timeStamp: bidAdUnit.timeStamp - }); - } + Object.keys(cache.auctions).forEach(auctionId => { + let auction = cache.auctions[auctionId]; + Object.keys(auction.bidAdUnits).forEach(adUnit => { + let bidAdUnit = auction.bidAdUnits[adUnit]; + if (!bidAdUnit.sent) { + bidAdUnit.sent = 1; + + bidAdUnits.push({ + adUnit: adUnit, + timeStamp: bidAdUnit.timeStamp + }); + } + }); }); return bidAdUnits; diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js index 5a9ef302d51..57e5a5a7c2f 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -9,7 +9,7 @@ export const storage = getStorageManager(); const BIDDER_CODE = 'livewrapped'; export const URL = 'https://lwadm.com/ad'; -const VERSION = '1.2'; +const VERSION = '1.3'; export const spec = { code: BIDDER_CODE, @@ -29,6 +29,7 @@ export const spec = { * seats: List of bidders and seats Optional. {"bidder name": ["seat 1", "seat 2"], ...} * deviceId: Device id if available Optional. * ifa: Advertising ID Optional. + * bundle: App bundle Optional. Read from config if exists. * options Dynamic data Optional. Optional data to send into adapter. * * @param {BidRequest} bid The bid params to validate. @@ -55,9 +56,10 @@ export const spec = { const seats = find(bidRequests, hasSeatsParam); const deviceId = find(bidRequests, hasDeviceIdParam); const ifa = find(bidRequests, hasIfaParam); + const bundle = find(bidRequests, hasBundleParam); const tid = find(bidRequests, hasTidParam); bidUrl = bidUrl ? bidUrl.params.bidUrl : URL; - url = url ? url.params.url : getTopWindowLocation(bidderRequest); + url = url ? url.params.url : (getAppDomain() || getTopWindowLocation(bidderRequest)); test = test ? test.params.test : undefined; var adRequests = bidRequests.map(bidToAdRequest); @@ -69,7 +71,10 @@ export const spec = { test: test, seats: seats ? seats.params.seats : undefined, deviceId: deviceId ? deviceId.params.deviceId : undefined, - ifa: ifa ? ifa.params.ifa : undefined, + ifa: ifa ? ifa.params.ifa : getDeviceIfa(), + bundle: bundle ? bundle.params.bundle : getAppBundle(), + width: getDeviceWidth(), + height: getDeviceHeight(), tid: tid ? tid.params.tid : undefined, version: VERSION, gdprApplies: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.gdprApplies : undefined, @@ -178,6 +183,10 @@ function hasIfaParam(bid) { return !!bid.params.ifa; } +function hasBundleParam(bid) { + return !!bid.params.bundle; +} + function hasTidParam(bid) { return !!bid.params.tid; } @@ -264,4 +273,40 @@ function getTopWindowLocation(bidderRequest) { return config.getConfig('pageUrl') || url; } +function getAppBundle() { + if (typeof config.getConfig('app') === 'object') { + return config.getConfig('app').bundle; + } +} + +function getAppDomain() { + if (typeof config.getConfig('app') === 'object') { + return config.getConfig('app').domain; + } +} + +function getDeviceIfa() { + if (typeof config.getConfig('device') === 'object') { + return config.getConfig('device').ifa; + } +} + +function getDeviceWidth() { + let device = config.getConfig('device'); + if (typeof device === 'object' && device.width) { + return device.width; + } + + return window.innerWidth; +} + +function getDeviceHeight() { + let device = config.getConfig('device'); + if (typeof device === 'object' && device.height) { + return device.height; + } + + return window.innerHeight; +} + registerBidder(spec); diff --git a/test/spec/modules/livewrappedBidAdapter_spec.js b/test/spec/modules/livewrappedBidAdapter_spec.js index 7b26f5b533d..b82bf8db160 100644 --- a/test/spec/modules/livewrappedBidAdapter_spec.js +++ b/test/spec/modules/livewrappedBidAdapter_spec.js @@ -100,7 +100,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -135,7 +137,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -171,7 +175,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'caller id 1', @@ -200,7 +206,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'panorama_d_1', @@ -228,7 +236,9 @@ describe('Livewrapped adapter tests', function () { let expectedQuery = { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -258,7 +268,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, deviceId: 'deviceid', ifa: 'ifa', cookieSupport: true, @@ -289,7 +301,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, tid: 'tracking id', test: true, cookieSupport: true, @@ -319,7 +333,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'panorama_d_1', @@ -348,7 +364,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, rcv: true, adRequests: [{ @@ -377,7 +395,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'panorama_d_1', @@ -406,7 +426,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'panorama_d_1', @@ -421,6 +443,53 @@ describe('Livewrapped adapter tests', function () { expect(data).to.deep.equal(expectedQuery); }); + it('should use app objects', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + + let testbidRequest = clone(bidderRequest); + delete testbidRequest.bids[0].params.url; + + let origGetConfig = config.getConfig; + sandbox.stub(config, 'getConfig').callsFake(function (key) { + if (key === 'app') { + return {bundle: 'bundle', domain: 'https://appdomain.com'}; + } + if (key === 'device') { + return {ifa: 'ifa', width: 300, height: 200}; + } + return origGetConfig.apply(config, arguments); + }); + + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://appdomain.com', + seats: {'dsp': ['seat 1']}, + version: '1.3', + width: 300, + height: 200, + ifa: 'ifa', + bundle: 'bundle', + cookieSupport: true, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + it('should use mediaTypes.banner.sizes before legacy sizes', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); @@ -436,7 +505,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'panorama_d_1', @@ -468,7 +539,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, gdprApplies: true, gdprConsent: 'test', @@ -502,7 +575,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, gdprApplies: false, adRequests: [{ @@ -531,7 +606,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: false, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -559,7 +636,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: false, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -620,7 +699,9 @@ describe('Livewrapped adapter tests', function () { userId: 'pubcid 123', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -650,7 +731,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', From b8d5e05d788c0041fd77689b4c15ce529b1eadd4 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Thu, 9 Apr 2020 09:02:42 -0600 Subject: [PATCH 84/90] Utils updates (#5092) * move parseQS to utils * move formatQS to utils * remove url module and combine with utils * remove old unused utils * update config to use query string helper * fix race condition --- modules/adkernelAdnAnalyticsAdapter.js | 7 +- modules/adkernelAdnBidAdapter.js | 3 +- modules/adkernelBidAdapter.js | 3 +- modules/advangelistsBidAdapter.js | 3 +- modules/adxcgAnalyticsAdapter.js | 3 +- modules/adxcgBidAdapter.js | 3 +- modules/adyoulikeBidAdapter.js | 4 +- modules/audienceNetworkBidAdapter.js | 3 +- modules/beachfrontBidAdapter.js | 3 +- modules/brightcomBidAdapter.js | 3 +- modules/criteoBidAdapter.js | 3 +- modules/criteoIdSystem.js | 3 +- modules/datablocksBidAdapter.js | 3 +- modules/dfpAdServerVideo.js | 5 +- modules/emoteevBidAdapter.js | 16 +- modules/emx_digitalBidAdapter.js | 3 +- modules/fintezaAnalyticsAdapter.js | 3 +- modules/konduitWrapper.js | 3 +- modules/medianetAnalyticsAdapter.js | 5 +- modules/medianetBidAdapter.js | 5 +- modules/mgidBidAdapter.js | 3 +- modules/mytargetBidAdapter.js | 3 +- modules/nafdigitalBidAdapter.js | 3 +- modules/nextrollBidAdapter.js | 3 +- modules/openxBidAdapter.js | 3 +- modules/priceFloors.js | 3 +- modules/pubCommonId.js | 5 +- modules/pubCommonIdSystem.js | 5 +- modules/readpeakBidAdapter.js | 3 +- modules/rubiconAnalyticsAdapter.js | 3 +- modules/sortableBidAdapter.js | 3 +- modules/sovrnBidAdapter.js | 3 +- modules/staqAnalyticsAdapter.js | 7 +- modules/theAdxBidAdapter.js | 5 +- modules/tpmnBidAdapter.js | 3 +- modules/undertoneBidAdapter.js | 4 +- modules/yuktamediaAnalyticsAdapter.js | 3 +- src/adserver.js | 2 +- src/ajax.js | 5 +- src/auction.js | 5 +- src/config.js | 3 +- src/events.js | 4 +- src/url.js | 55 ----- src/utils.js | 212 +++++------------- test/spec/modules/adformBidAdapter_spec.js | 1 - .../modules/adformOpenRTBBidAdapter_spec.js | 1 - test/spec/modules/adxcgBidAdapter_spec.js | 21 +- test/spec/modules/adyoulikeBidAdapter_spec.js | 1 - .../spec/modules/beachfrontBidAdapter_spec.js | 2 +- test/spec/modules/criteoIdSystem_spec.js | 3 +- test/spec/modules/dfpAdServerVideo_spec.js | 43 ++-- test/spec/modules/emoteevBidAdapter_spec.js | 41 ++-- .../modules/eplanningAnalyticsAdapter_spec.js | 4 +- test/spec/modules/etargetBidAdapter_spec.js | 1 - .../modules/fintezaAnalyticsAdapter_spec.js | 12 +- test/spec/modules/konduitWrapper_spec.js | 2 +- test/spec/modules/mgidBidAdapter_spec.js | 11 +- test/spec/modules/quantcastBidAdapter_spec.js | 6 +- test/spec/modules/readpeakBidAdapter_spec.js | 2 +- .../spec/modules/revcontentBidAdapter_spec.js | 1 - .../modules/seedingAllianceAdapter_spec.js | 1 - test/spec/unit/core/bidderFactory_spec.js | 4 +- test/spec/url_spec.js | 94 -------- test/spec/utils_spec.js | 126 ++++++++--- 64 files changed, 290 insertions(+), 519 deletions(-) delete mode 100644 src/url.js delete mode 100644 test/spec/url_spec.js mode change 100755 => 100644 test/spec/utils_spec.js diff --git a/modules/adkernelAdnAnalyticsAdapter.js b/modules/adkernelAdnAnalyticsAdapter.js index b0bdadf57d0..b6db6fc22de 100644 --- a/modules/adkernelAdnAnalyticsAdapter.js +++ b/modules/adkernelAdnAnalyticsAdapter.js @@ -1,7 +1,6 @@ import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import {parse} from '../src/url.js'; import * as utils from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -211,7 +210,7 @@ export function getUmtSource(pageUrl, referrer) { if (se) { return asUtm(se, ORGANIC, ORGANIC); } - let parsedUrl = parse(pageUrl); + let parsedUrl = utils.parseUrl(pageUrl); let [refHost, refPath] = getReferrer(referrer); if (refHost && refHost !== parsedUrl.hostname) { return asUtm(refHost, REFERRAL, REFERRAL, '', refPath); @@ -238,12 +237,12 @@ export function getUmtSource(pageUrl, referrer) { } function getReferrer(referrer) { - let ref = parse(referrer); + let ref = utils.parseUrl(referrer); return [ref.hostname, ref.pathname]; } function getUTM(pageUrl) { - let urlParameters = parse(pageUrl).search; + let urlParameters = utils.parseUrl(pageUrl).search; if (!urlParameters['utm_campaign'] || !urlParameters['utm_source']) { return; } diff --git a/modules/adkernelAdnBidAdapter.js b/modules/adkernelAdnBidAdapter.js index 25a7f2f4abc..483d6de52b9 100644 --- a/modules/adkernelAdnBidAdapter.js +++ b/modules/adkernelAdnBidAdapter.js @@ -1,7 +1,6 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {parse as parseUrl} from '../src/url.js'; const DEFAULT_ADKERNEL_DSP_DOMAIN = 'tag.adkernel.com'; const DEFAULT_MIMES = ['video/mp4', 'video/webm', 'application/x-shockwave-flash', 'application/javascript']; @@ -71,7 +70,7 @@ function buildRequestParams(tags, auctionId, transactionId, gdprConsent, uspCons } function buildSite(refInfo) { - let loc = parseUrl(refInfo.referer); + let loc = utils.parseUrl(refInfo.referer); let result = { page: `${loc.protocol}://${loc.hostname}${loc.pathname}`, secure: ~~(loc.protocol === 'https') diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index dbb0f4a3344..1a16801ba67 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -3,7 +3,6 @@ import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import find from 'core-js/library/fn/array/find.js'; import includes from 'core-js/library/fn/array/includes.js'; -import {parse as parseUrl} from '../src/url.js'; /* * In case you're AdKernel whitelable platform's client who needs branded adapter to @@ -274,7 +273,7 @@ function getLanguage() { * Creates site description object */ function createSite(refInfo) { - let url = parseUrl(refInfo.referer); + let url = utils.parseUrl(refInfo.referer); let site = { 'domain': url.hostname, 'page': `${url.protocol}://${url.hostname}${url.pathname}` diff --git a/modules/advangelistsBidAdapter.js b/modules/advangelistsBidAdapter.js index b3064568d3a..64e4c58653b 100644 --- a/modules/advangelistsBidAdapter.js +++ b/modules/advangelistsBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js'; -import { parse as parseUrl } from '../src/url.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; @@ -293,7 +292,7 @@ function createVideoRequestData(bid, bidderRequest) { function getTopWindowLocation(bidderRequest) { let url = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer; - return parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); + return utils.parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); } function createBannerRequestData(bid, bidderRequest) { diff --git a/modules/adxcgAnalyticsAdapter.js b/modules/adxcgAnalyticsAdapter.js index 01baa0ebd46..9f514c545a1 100644 --- a/modules/adxcgAnalyticsAdapter.js +++ b/modules/adxcgAnalyticsAdapter.js @@ -2,7 +2,6 @@ import { ajax } from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import * as url from '../src/url.js'; import * as utils from '../src/utils.js'; /** @@ -113,7 +112,7 @@ function mapBidWon (bidResponse) { } function send (data) { - let adxcgAnalyticsRequestUrl = url.format({ + let adxcgAnalyticsRequestUrl = utils.buildUrl({ protocol: 'https', hostname: adxcgAnalyticsAdapter.context.host, pathname: '/pbrx/v2', diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index 37690c66d58..cc76731836b 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -1,6 +1,5 @@ import { config } from '../src/config.js' import * as utils from '../src/utils.js' -import * as url from '../src/url.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js' import includes from 'core-js/library/fn/array/includes.js' @@ -179,7 +178,7 @@ export const spec = { beaconParams.idl_env = validBidRequests[0].userId.idl_env; } - let adxcgRequestUrl = url.format({ + let adxcgRequestUrl = utils.buildUrl({ protocol: 'https', hostname: 'hbps.adxcg.net', pathname: '/get/adi', diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 7e591b3901c..9391a3fb94f 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,6 +1,4 @@ import * as utils from '../src/utils.js'; -import { format } from '../src/url.js'; -// import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import find from 'core-js/library/fn/array/find.js'; @@ -145,7 +143,7 @@ function getPageRefreshed() { /* Create endpoint url */ function createEndpoint(bidRequests, bidderRequest) { let host = getHostname(bidRequests); - return format({ + return utils.buildUrl({ protocol: 'https', host: `${DEFAULT_DC}${host}.omnitagjs.com`, pathname: '/hb-api/prebid/v1', diff --git a/modules/audienceNetworkBidAdapter.js b/modules/audienceNetworkBidAdapter.js index 54a50af4269..2d7b7eae0dc 100644 --- a/modules/audienceNetworkBidAdapter.js +++ b/modules/audienceNetworkBidAdapter.js @@ -2,8 +2,7 @@ * @file AudienceNetwork adapter. */ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { formatQS } from '../src/url.js'; -import { generateUUID, deepAccess, convertTypes } from '../src/utils.js'; +import { generateUUID, deepAccess, convertTypes, formatQS } from '../src/utils.js'; import findIndex from 'core-js/library/fn/array/find-index.js'; import includes from 'core-js/library/fn/array/includes.js'; diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index bbb4ae35729..9b6c431fdd7 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js'; -import { parse as parseUrl } from '../src/url.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; @@ -247,7 +246,7 @@ function isBannerBidValid(bid) { function getTopWindowLocation(bidderRequest) { let url = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer; - return parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); + return utils.parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); } function getTopWindowReferrer() { diff --git a/modules/brightcomBidAdapter.js b/modules/brightcomBidAdapter.js index 0a418406d4f..a4b013a2fe2 100644 --- a/modules/brightcomBidAdapter.js +++ b/modules/brightcomBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js'; -import * as url from '../src/url.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; @@ -57,7 +56,7 @@ function buildRequests(bidReqs, bidderRequest) { id: utils.getUniqueIdentifierStr(), imp: brightcomImps, site: { - domain: url.parse(referrer).host, + domain: utils.parseUrl(referrer).host, page: referrer, publisher: { id: publisherId diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 7c2c5d69ad3..6eca0c95dde 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -2,7 +2,6 @@ import { loadExternalScript } from '../src/adloader.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { parse } from '../src/url.js'; import * as utils from '../src/utils.js'; import find from 'core-js/library/fn/array/find.js'; import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; @@ -204,7 +203,7 @@ function buildContext(bidRequests, bidderRequest) { if (bidderRequest && bidderRequest.refererInfo) { referrer = bidderRequest.refererInfo.referer; } - const queryString = parse(referrer).search; + const queryString = utils.parseUrl(referrer).search; const context = { url: referrer, diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index d03e5de26c7..c44f0c843ae 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -7,7 +7,6 @@ import * as utils from '../src/utils.js' import * as ajax from '../src/ajax.js' -import * as urlLib from '../src/url.js' import { getRefererInfo } from '../src/refererDetection.js' import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -30,7 +29,7 @@ function areCookiesWriteable() { } function extractProtocolHost (url, returnOnlyHost = false) { - const parsedUrl = urlLib.parse(url) + const parsedUrl = utils.parseUrl(url) return returnOnlyHost ? `${parsedUrl.hostname}` : `${parsedUrl.protocol}://${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}/`; diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 32b18592417..b00a3eae659 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -1,7 +1,6 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { parse as parseUrl } from '../src/url.js'; const NATIVE_MAP = { 'body': 2, 'body2': 10, @@ -61,7 +60,7 @@ export const spec = { let imps = {}; let site = {}; let device = {}; - let refurl = parseUrl(bidderRequest.referrer); + let refurl = utils.parseUrl(bidderRequest.referrer); let requests = []; validBidRequests.forEach(bidRequest => { diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 426da05efcf..71a8471d554 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -4,8 +4,7 @@ import { registerVideoSupport } from '../src/adServerManager.js'; import { targeting } from '../src/targeting.js'; -import { formatQS, format as buildUrl, parse } from '../src/url.js'; -import { deepAccess, isEmpty, logError, parseSizesInput } from '../src/utils.js'; +import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, buildUrl } from '../src/utils.js'; import { config } from '../src/config.js'; import { getHook, submodule } from '../src/hook.js'; import { auctionManager } from '../src/auctionManager.js'; @@ -74,7 +73,7 @@ export function buildDfpVideoUrl(options) { if (options.url) { // when both `url` and `params` are given, parsed url will be overwriten // with any matching param components - urlComponents = parse(options.url, {noDecodeWholeURL: true}); + urlComponents = parseUrl(options.url, {noDecodeWholeURL: true}); if (isEmpty(options.params)) { return buildUrlFromAdserverUrlComponents(urlComponents, bid, options); diff --git a/modules/emoteevBidAdapter.js b/modules/emoteevBidAdapter.js index 0bdf4cda58e..e0f88725d8a 100644 --- a/modules/emoteevBidAdapter.js +++ b/modules/emoteevBidAdapter.js @@ -23,10 +23,10 @@ import { deepAccess, isArray, isInteger, - getParameterByName + getParameterByName, + buildUrl } from '../src/utils.js'; import {config} from '../src/config.js'; -import * as url from '../src/url.js'; import { getStorageManager } from '../src/storageManager.js'; export const storage = getStorageManager(); @@ -234,7 +234,7 @@ export const domain = (env) => { * @param {string} env Emoteev environment parameter * @returns {string} The full URL which events is sent to. */ -export const eventsUrl = env => url.format({ +export const eventsUrl = env => buildUrl({ protocol: (env === DEVELOPMENT) ? 'http' : 'https', hostname: domain(env), pathname: EVENTS_PATH @@ -246,7 +246,7 @@ export const eventsUrl = env => url.format({ * @param {string} env Emoteev environment parameter * @returns {string} The full URL which bidderRequest is sent to. */ -export const bidderUrl = env => url.format({ +export const bidderUrl = env => buildUrl({ protocol: (env === DEVELOPMENT) ? 'http' : 'https', hostname: domain(env), pathname: BIDDER_PATH @@ -258,7 +258,7 @@ export const bidderUrl = env => url.format({ * @param {string} env Emoteev environment parameter * @returns {string} The full URL called for iframe-based user sync */ -export const userSyncIframeUrl = env => url.format({ +export const userSyncIframeUrl = env => buildUrl({ protocol: (env === DEVELOPMENT) ? 'http' : 'https', hostname: domain(env), pathname: USER_SYNC_IFRAME_PATH @@ -270,7 +270,7 @@ export const userSyncIframeUrl = env => url.format({ * @param {string} env Emoteev environment parameter * @returns {string} The full URL called for image-based user sync */ -export const userSyncImageUrl = env => url.format({ +export const userSyncImageUrl = env => buildUrl({ protocol: (env === DEVELOPMENT) ? 'http' : 'https', hostname: domain(env), pathname: USER_SYNC_IMAGE_PATH @@ -508,12 +508,12 @@ export const spec = { bidderRequest), interpretResponse: interpretResponse, onBidWon: (bidObject) => - triggerPixel(url.format(onBidWon( + triggerPixel(buildUrl(onBidWon( resolveEnv(config.getConfig(), getParameterByName('emoteevEnv')), storage.getCookie('_pubcid'), bidObject))), onTimeout: (bidRequest) => - triggerPixel(url.format(onTimeout( + triggerPixel(buildUrl(onTimeout( resolveEnv(config.getConfig(), getParameterByName('emoteevEnv')), bidRequest))), getUserSyncs: (syncOptions) => diff --git a/modules/emx_digitalBidAdapter.js b/modules/emx_digitalBidAdapter.js index 22a7bb6f643..83689aa76f1 100644 --- a/modules/emx_digitalBidAdapter.js +++ b/modules/emx_digitalBidAdapter.js @@ -3,7 +3,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { Renderer } from '../src/Renderer.js'; import includes from 'core-js/library/fn/array/includes.js'; -import {parse as parseUrl} from '../src/url.js'; const BIDDER_CODE = 'emx_digital'; const ENDPOINT = 'hb.emxdgt.com'; @@ -130,7 +129,7 @@ export const emxAdapter = { } }, getSite: (refInfo) => { - let url = parseUrl(refInfo.referer); + let url = utils.parseUrl(refInfo.referer); return { domain: url.hostname, page: refInfo.referer, diff --git a/modules/fintezaAnalyticsAdapter.js b/modules/fintezaAnalyticsAdapter.js index f454e6b3acb..8729035491f 100644 --- a/modules/fintezaAnalyticsAdapter.js +++ b/modules/fintezaAnalyticsAdapter.js @@ -2,7 +2,6 @@ import { ajax } from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; -import { parse as parseURL } from '../src/url.js'; import { getStorageManager } from '../src/storageManager.js'; const storage = getStorageManager(); @@ -29,7 +28,7 @@ function getPageInfo() { } if (document.referrer) { - pageInfo.referrerDomain = parseURL(document.referrer).hostname; + pageInfo.referrerDomain = utils.parseUrl(document.referrer).hostname; } return pageInfo; diff --git a/modules/konduitWrapper.js b/modules/konduitWrapper.js index c4cb4c8ff81..33bc45f7566 100644 --- a/modules/konduitWrapper.js +++ b/modules/konduitWrapper.js @@ -1,6 +1,5 @@ import { registerVideoSupport } from '../src/adServerManager.js'; import { targeting } from '../src/targeting.js'; -import { format as buildUrl } from '../src/url.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; @@ -52,7 +51,7 @@ export function buildVastUrl(options) { let resultingUrl = null; if (queryParams.konduit_url) { - resultingUrl = buildUrl({ + resultingUrl = utils.buildUrl({ protocol: 'https', host: 'p.konduit.me', pathname: '/api/vastProxy', diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js index 35a050831f3..3da69be92eb 100644 --- a/modules/medianetAnalyticsAdapter.js +++ b/modules/medianetAnalyticsAdapter.js @@ -4,7 +4,6 @@ import CONSTANTS from '../src/constants.json'; import * as utils from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { getRefererInfo } from '../src/refererDetection.js'; -import * as url from '../src/url.js'; import { getPriceGranularity, AUCTION_IN_PROGRESS, AUCTION_COMPLETED } from '../src/auction.js' const analyticsType = 'endpoint'; @@ -120,7 +119,7 @@ class Configure { init() { // Forces Logging % to 100% - let urlObj = url.parse(pageDetails.page); + let urlObj = utils.parseUrl(pageDetails.page); if (utils.deepAccess(urlObj, 'search.medianet_test') || urlObj.hostname === 'localhost') { this.loggingPercent = 100; this.ajaxState = CONFIG_PASS; @@ -144,7 +143,7 @@ class PageDetail { const twitterUrl = this._getUrlFromSelector('meta[name="twitter:url"]', 'content'); const refererInfo = getRefererInfo(); - this.domain = url.parse(refererInfo.referer).host; + this.domain = utils.parseUrl(refererInfo.referer).host; this.page = refererInfo.referer; this.is_top = refererInfo.reachedTop; this.referrer = this._getTopWindowReferrer(); diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index d944f354c83..a7434cac6ab 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -1,7 +1,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; -import * as url from '../src/url.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { getRefererInfo } from '../src/refererDetection.js'; @@ -22,7 +21,7 @@ let refererInfo = getRefererInfo(); let mnData = {}; mnData.urlData = { - domain: url.parse(refererInfo.referer).host, + domain: utils.parseUrl(refererInfo.referer).host, page: refererInfo.referer, isTop: refererInfo.reachedTop } @@ -287,7 +286,7 @@ function logEvent (event, data) { hostname: EVENT_PIXEL_URL, search: getLoggingData(event, data) }; - utils.triggerPixel(url.format(getParams)); + utils.triggerPixel(utils.buildUrl(getParams)); } function clearMnData() { diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 194a8255641..957b9a1d703 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -1,6 +1,5 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; -import * as urlUtils from '../src/url.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -123,7 +122,7 @@ export const spec = { } const info = pageInfo(); const page = info.location || utils.deepAccess(bidderRequest, 'refererInfo.referer') || utils.deepAccess(bidderRequest, 'refererInfo.canonicalUrl'); - const hostname = urlUtils.parse(page).hostname; + const hostname = utils.parseUrl(page).hostname; let domain = extractDomainFromHost(hostname) || hostname; const accountId = setOnAny(validBidRequests, 'params.accountId'); const muid = getLocalStorageSafely('mgMuidn'); diff --git a/modules/mytargetBidAdapter.js b/modules/mytargetBidAdapter.js index 24ad488d382..bcf8e7ad17f 100644 --- a/modules/mytargetBidAdapter.js +++ b/modules/mytargetBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js'; -import * as url from '../src/url.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -29,7 +28,7 @@ function getSiteName(referrer) { let sitename = config.getConfig('mytarget.sitename'); if (!sitename) { - sitename = url.parse(referrer).hostname; + sitename = utils.parseUrl(referrer).hostname; } return sitename; diff --git a/modules/nafdigitalBidAdapter.js b/modules/nafdigitalBidAdapter.js index 2be845d6fdd..d64e079b52a 100644 --- a/modules/nafdigitalBidAdapter.js +++ b/modules/nafdigitalBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js'; -import * as url from '../src/url.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; @@ -56,7 +55,7 @@ function buildRequests(bidReqs, bidderRequest) { id: utils.getUniqueIdentifierStr(), imp: nafdigitalImps, site: { - domain: url.parse(referrer).host, + domain: utils.parseUrl(referrer).host, page: referrer, publisher: { id: publisherId diff --git a/modules/nextrollBidAdapter.js b/modules/nextrollBidAdapter.js index e85efceba68..81c48102bb4 100644 --- a/modules/nextrollBidAdapter.js +++ b/modules/nextrollBidAdapter.js @@ -1,7 +1,6 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -import { parse as parseUrl } from '../src/url.js'; import find from 'core-js/library/fn/array/find.js'; @@ -30,7 +29,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { - let topLocation = parseUrl(utils.deepAccess(bidderRequest, 'refererInfo.referer')); + let topLocation = utils.parseUrl(utils.deepAccess(bidderRequest, 'refererInfo.referer')); let consent = hasCCPAConsent(bidderRequest); return validBidRequests.map((bidRequest, index) => { return { diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index a469a71ecb2..c4a90fb0930 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -2,7 +2,6 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {parse} from '../src/url.js'; const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'openx'; @@ -424,7 +423,7 @@ function createVideoBidResponses(response, {bid, startTime}) { let bidResponses = []; if (response !== undefined && response.vastUrl !== '' && response.pub_rev !== '') { - let vastQueryParams = parse(response.vastUrl).search || {}; + let vastQueryParams = utils.parseUrl(response.vastUrl).search || {}; let bidResponse = {}; bidResponse.requestId = bid.bidId; // default 5 mins diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 2ee5fb3b0fd..0accd48d003 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -7,7 +7,6 @@ import CONSTANTS from '../src/constants.json'; import { getHook } from '../src/hook.js'; import { createBid } from '../src/bidfactory.js'; import find from 'core-js/library/fn/array/find.js'; -import { parse as urlParse } from '../src/url.js'; import { getRefererInfo } from '../src/refererDetection.js'; /** @@ -61,7 +60,7 @@ function roundUp(number, precision) { let referrerHostname; function getHostNameFromReferer(referer) { - referrerHostname = urlParse(referer, {noDecodeWholeURL: true}).hostname; + referrerHostname = utils.parseUrl(referer, {noDecodeWholeURL: true}).hostname; return referrerHostname; } diff --git a/modules/pubCommonId.js b/modules/pubCommonId.js index d69ce0f2086..174fa6ffe6e 100644 --- a/modules/pubCommonId.js +++ b/modules/pubCommonId.js @@ -6,7 +6,6 @@ import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; import events from '../src/events.js'; -import * as url from '../src/url.js'; import CONSTANTS from '../src/constants.json'; import { getStorageManager } from '../src/storageManager.js'; @@ -142,9 +141,9 @@ function queuePixelCallback(pixelUrl, id) { id = id || ''; // Use pubcid as a cache buster - const urlInfo = url.parse(pixelUrl); + const urlInfo = utils.parseUrl(pixelUrl); urlInfo.search.id = encodeURIComponent('pubcid:' + id); - const targetUrl = url.format(urlInfo); + const targetUrl = utils.buildUrl(urlInfo); events.on(CONSTANTS.EVENTS.AUCTION_END, function auctionEndHandler() { events.off(CONSTANTS.EVENTS.AUCTION_END, auctionEndHandler); diff --git a/modules/pubCommonIdSystem.js b/modules/pubCommonIdSystem.js index b12d981c2ba..8e2be1207f5 100644 --- a/modules/pubCommonIdSystem.js +++ b/modules/pubCommonIdSystem.js @@ -6,7 +6,6 @@ */ import * as utils from '../src/utils.js'; -import * as url from '../src/url.js'; import {submodule} from '../src/hook.js'; const PUB_COMMON_ID = 'PublisherCommonId'; @@ -32,9 +31,9 @@ export const pubCommonIdSubmodule = { } // Use pubcid as a cache buster - const urlInfo = url.parse(pixelUrl); + const urlInfo = utils.parseUrl(pixelUrl); urlInfo.search.id = encodeURIComponent('pubcid:' + id); - const targetUrl = url.format(urlInfo); + const targetUrl = utils.buildUrl(urlInfo); return function () { utils.triggerPixel(targetUrl); diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index 56006aea144..c72bbdd658f 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -1,8 +1,7 @@ -import { logError, replaceAuctionPrice } from '../src/utils.js'; +import { logError, replaceAuctionPrice, parseUrl } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { NATIVE } from '../src/mediaTypes.js'; -import { parse as parseUrl } from '../src/url.js'; export const ENDPOINT = 'https://app.readpeak.com/header/prebid'; diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 80c881f479a..28c11ef8264 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -4,7 +4,6 @@ import CONSTANTS from '../src/constants.json'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import * as utils from '../src/utils.js'; -import * as urlLib from '../src/url.js' import { getGlobal } from '../src/prebidGlobal.js'; const { @@ -43,7 +42,7 @@ const cache = { export function getHostNameFromReferer(referer) { try { - rubiconAdapter.referrerHostname = urlLib.parse(referer, {noDecodeWholeURL: true}).hostname; + rubiconAdapter.referrerHostname = utils.parseUrl(referer, {noDecodeWholeURL: true}).hostname; } catch (e) { utils.logError('Rubicon Analytics: Unable to parse hostname from supplied url: ', referer, e); rubiconAdapter.referrerHostname = ''; diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js index 9fbb02ad08f..f5d3a6b1bb5 100644 --- a/modules/sortableBidAdapter.js +++ b/modules/sortableBidAdapter.js @@ -1,7 +1,6 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; -import { parse } from '../src/url.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'sortable'; @@ -157,7 +156,7 @@ export const spec = { buildRequests: function(validBidReqs, bidderRequest) { const sortableConfig = config.getConfig('sortable') || {}; const globalSiteId = sortableConfig.siteId; - let loc = parse(bidderRequest.refererInfo.referer); + let loc = utils.parseUrl(bidderRequest.refererInfo.referer); const sortableImps = utils._map(validBidReqs, bid => { const rv = { diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 2566f314018..98c8c8e3b33 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js' -import {parse} from '../src/url.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER } from '../src/mediaTypes.js' @@ -62,7 +61,7 @@ export const spec = { const page = bidderRequest.refererInfo.referer // clever trick to get the domain - const domain = parse(page).hostname + const domain = utils.parseUrl(page).hostname const sovrnBidReq = { id: utils.getUniqueIdentifierStr(), diff --git a/modules/staqAnalyticsAdapter.js b/modules/staqAnalyticsAdapter.js index 31b231637f4..4e3914bccdd 100644 --- a/modules/staqAnalyticsAdapter.js +++ b/modules/staqAnalyticsAdapter.js @@ -2,7 +2,6 @@ import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; -import { parse } from '../src/url.js'; import * as utils from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -249,7 +248,7 @@ export function getUmtSource(pageUrl, referrer) { if (se) { return asUtm(se, ORGANIC, ORGANIC); } - let parsedUrl = parse(pageUrl); + let parsedUrl = utils.parseUrl(pageUrl); let [refHost, refPath] = getReferrer(referrer); if (refHost && refHost !== parsedUrl.hostname) { return asUtm(refHost, REFERRAL, REFERRAL, '', refPath); @@ -276,12 +275,12 @@ export function getUmtSource(pageUrl, referrer) { } function getReferrer(referrer) { - let ref = parse(referrer); + let ref = utils.parseUrl(referrer); return [ref.hostname, ref.pathname]; } function getUTM(pageUrl) { - let urlParameters = parse(pageUrl).search; + let urlParameters = utils.parseUrl(pageUrl).search; if (!urlParameters['utm_campaign'] || !urlParameters['utm_source']) { return; } diff --git a/modules/theAdxBidAdapter.js b/modules/theAdxBidAdapter.js index 5306b89a5da..91e36077e88 100644 --- a/modules/theAdxBidAdapter.js +++ b/modules/theAdxBidAdapter.js @@ -7,9 +7,6 @@ import { import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { - parse as parseUrl -} from '../src/url.js'; const BIDDER_CODE = 'theadx'; const ENDPOINT_URL = 'https://ssp.theadx.com/request'; @@ -317,7 +314,7 @@ export const spec = { } let buildSiteComponent = (bidRequest, bidderRequest) => { - let loc = parseUrl(bidderRequest.refererInfo.referer, { + let loc = utils.parseUrl(bidderRequest.refererInfo.referer, { decodeSearchAsString: true }); diff --git a/modules/tpmnBidAdapter.js b/modules/tpmnBidAdapter.js index d80c87987fa..ec9d30c0e29 100644 --- a/modules/tpmnBidAdapter.js +++ b/modules/tpmnBidAdapter.js @@ -2,7 +2,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; -import {parse as parseUrl} from '../src/url.js'; export const ADAPTER_VERSION = '1'; const SUPPORTED_AD_TYPES = [BANNER]; @@ -73,7 +72,7 @@ registerBidder(spec); * Creates site description object */ function createSite(refInfo) { - let url = parseUrl(refInfo.referer); + let url = utils.parseUrl(refInfo.referer); let site = { 'domain': url.hostname, 'page': url.protocol + '://' + url.hostname + url.pathname diff --git a/modules/undertoneBidAdapter.js b/modules/undertoneBidAdapter.js index 923e54183ab..6ead453b622 100644 --- a/modules/undertoneBidAdapter.js +++ b/modules/undertoneBidAdapter.js @@ -2,7 +2,7 @@ * Adapter to send bids to Undertone */ -import * as urlUtils from '../src/url.js'; +import { parseUrl } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'undertone'; @@ -92,7 +92,7 @@ export const spec = { } }; const referer = bidderRequest.refererInfo.referer; - const hostname = urlUtils.parse(referer).hostname; + const hostname = parseUrl(referer).hostname; let domain = extractDomainFromHost(hostname); const pageUrl = getCanonicalUrl() || referer; diff --git a/modules/yuktamediaAnalyticsAdapter.js b/modules/yuktamediaAnalyticsAdapter.js index 268f31ea402..b346a26c843 100644 --- a/modules/yuktamediaAnalyticsAdapter.js +++ b/modules/yuktamediaAnalyticsAdapter.js @@ -2,7 +2,6 @@ import { ajax } from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import * as url from '../src/url.js'; import * as utils from '../src/utils.js'; const emptyUrl = ''; @@ -115,7 +114,7 @@ function send(data, status) { } data.initOptions = initOptions; - let yuktamediaAnalyticsRequestUrl = url.format({ + let yuktamediaAnalyticsRequestUrl = utils.buildUrl({ protocol: 'https', hostname: 'analytics-prebid.yuktamedia.com', pathname: status == 'auctionEnd' ? '/api/bids' : '/api/bid/won', diff --git a/src/adserver.js b/src/adserver.js index c189037a9da..61af8862972 100644 --- a/src/adserver.js +++ b/src/adserver.js @@ -1,4 +1,4 @@ -import { formatQS } from './url.js'; +import { formatQS } from './utils.js'; import { targeting } from './targeting.js'; // Adserver parent class diff --git a/src/ajax.js b/src/ajax.js index 16f7b365092..b1e4cbdbdff 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -1,4 +1,3 @@ -import {parse as parseURL, format as formatURL} from './url.js'; import { config } from './config.js'; var utils = require('./utils.js'); @@ -61,9 +60,9 @@ export function ajaxBuilder(timeout = 3000, {request, done} = {}) { } if (method === 'GET' && data) { - let urlInfo = parseURL(url, options); + let urlInfo = utils.parseUrl(url, options); Object.assign(urlInfo.search, data); - url = formatURL(urlInfo); + url = utils.buildUrl(urlInfo); } x.open(method, url, true); diff --git a/src/auction.js b/src/auction.js index 628c164a537..d953763cea8 100644 --- a/src/auction.js +++ b/src/auction.js @@ -57,8 +57,7 @@ * @property {function(): void} callBids - sends requests to all adapters for bids */ -import {flatten, timestamp, adUnitsFilter, deepAccess, getBidRequest, getValue} from './utils.js'; -import { parse as parseURL } from './url.js'; +import {flatten, timestamp, adUnitsFilter, deepAccess, getBidRequest, getValue, parseUrl} from './utils.js'; import { getPriceBucketString } from './cpmBucketManager.js'; import { getNativeTargeting } from './native.js'; import { getCacheUrl, store } from './videoCache.js'; @@ -651,7 +650,7 @@ export function getStandardBidderSettings(mediaType, bidderCode, bidReq) { // Adding hb_cache_host if (config.getConfig('cache.url') && (!bidderCode || utils.deepAccess(bidderSettings, `${bidderCode}.sendStandardTargeting`) !== false)) { - const urlInfo = parseURL(config.getConfig('cache.url')); + const urlInfo = parseUrl(config.getConfig('cache.url')); if (typeof find(adserverTargeting, targetingKeyVal => targetingKeyVal.key === TARGETING_KEYS.CACHE_HOST) === 'undefined') { adserverTargeting.push(createKeyVal(TARGETING_KEYS.CACHE_HOST, function(bidResponse) { diff --git a/src/config.js b/src/config.js index 24962bdff37..521171cbf7d 100644 --- a/src/config.js +++ b/src/config.js @@ -16,13 +16,12 @@ import { isValidPriceConfig } from './cpmBucketManager.js'; import find from 'core-js/library/fn/array/find.js'; import includes from 'core-js/library/fn/array/includes.js'; import Set from 'core-js/library/fn/set.js'; -import { parseQS } from './url.js'; const from = require('core-js/library/fn/array/from.js'); const utils = require('./utils.js'); const CONSTANTS = require('./constants.json'); -const DEFAULT_DEBUG = (parseQS(window.location.search)[CONSTANTS.DEBUG_MODE] || '').toUpperCase() === 'TRUE'; +const DEFAULT_DEBUG = utils.getParameterByName(CONSTANTS.DEBUG_MODE).toUpperCase() === 'TRUE'; const DEFAULT_BIDDER_TIMEOUT = 3000; const DEFAULT_PUBLISHER_DOMAIN = window.location.origin; const DEFAULT_ENABLE_SEND_ALL_BIDS = true; diff --git a/src/events.js b/src/events.js index 2d99ed4c561..e7a11635476 100644 --- a/src/events.js +++ b/src/events.js @@ -113,14 +113,14 @@ module.exports = (function () { utils._each(event[id].que, function (_handler) { var que = event[id].que; if (_handler === handler) { - que.splice(utils.indexOf.call(que, _handler), 1); + que.splice(que.indexOf(_handler), 1); } }); } else { utils._each(event.que, function (_handler) { var que = event.que; if (_handler === handler) { - que.splice(utils.indexOf.call(que, _handler), 1); + que.splice(que.indexOf(_handler), 1); } }); } diff --git a/src/url.js b/src/url.js deleted file mode 100644 index c63bca2ca41..00000000000 --- a/src/url.js +++ /dev/null @@ -1,55 +0,0 @@ -export function parseQS(query) { - return !query ? {} : query - .replace(/^\?/, '') - .split('&') - .reduce((acc, criteria) => { - let [k, v] = criteria.split('='); - if (/\[\]$/.test(k)) { - k = k.replace('[]', ''); - acc[k] = acc[k] || []; - acc[k].push(v); - } else { - acc[k] = v || ''; - } - return acc; - }, {}); -} - -export function formatQS(query) { - return Object - .keys(query) - .map(k => Array.isArray(query[k]) - ? query[k].map(v => `${k}[]=${v}`).join('&') - : `${k}=${query[k]}`) - .join('&'); -} - -export function parse(url, options) { - let parsed = document.createElement('a'); - if (options && 'noDecodeWholeURL' in options && options.noDecodeWholeURL) { - parsed.href = url; - } else { - parsed.href = decodeURIComponent(url); - } - // in window.location 'search' is string, not object - let qsAsString = (options && 'decodeSearchAsString' in options && options.decodeSearchAsString); - return { - href: parsed.href, - protocol: (parsed.protocol || '').replace(/:$/, ''), - hostname: parsed.hostname, - port: +parsed.port, - pathname: parsed.pathname.replace(/^(?!\/)/, '/'), - search: (qsAsString) ? parsed.search : parseQS(parsed.search || ''), - hash: (parsed.hash || '').replace(/^#/, ''), - host: parsed.host || window.location.host - }; -} - -export function format(obj) { - return (obj.protocol || 'http') + '://' + - (obj.host || - obj.hostname + (obj.port ? `:${obj.port}` : '')) + - (obj.pathname || '') + - (obj.search ? `?${formatQS(obj.search || '')}` : '') + - (obj.hash ? `#${obj.hash}` : ''); -} diff --git a/src/utils.js b/src/utils.js index 45822dd7088..0fe924510e0 100644 --- a/src/utils.js +++ b/src/utils.js @@ -38,6 +38,8 @@ export const internal = { logWarn, logMessage, logInfo, + parseQS, + formatQS, deepEqual }; @@ -52,28 +54,6 @@ export let bind = function(a, b) { return b; }.bind(null, 1, uniqueRef)() === un }; }; -/* - * Substitutes into a string from a given map using the token - * Usage - * var str = 'text %%REPLACE%% this text with %%SOMETHING%%'; - * var map = {}; - * map['replace'] = 'it was subbed'; - * map['something'] = 'something else'; - * console.log(replaceTokenInString(str, map, '%%')); => "text it was subbed this text with something else" - */ -export function replaceTokenInString(str, map, token) { - _each(map, function (value, key) { - value = (value === undefined) ? '' : value; - - var keyString = token + key.toUpperCase() + token; - var re = new RegExp(keyString, 'g'); - - str = str.replace(re, value); - }); - - return str; -} - /* utility method to get incremental integer starting from 1 */ var getIncrementalInteger = (function () { var count = 0; @@ -122,7 +102,7 @@ export function getBidIdParameter(key, paramsObj) { export function tryAppendQueryString(existingUrl, key, value) { if (value) { - return existingUrl += key + '=' + encodeURIComponent(value) + '&'; + return existingUrl + key + '=' + encodeURIComponent(value) + '&'; } return existingUrl; @@ -130,14 +110,8 @@ export function tryAppendQueryString(existingUrl, key, value) { // parse a query string object passed in bid params // bid params should be an object such as {key: "value", key1 : "value1"} -export function parseQueryStringParameters(queryObj) { - let result = ''; - for (var k in queryObj) { - if (queryObj.hasOwnProperty(k)) { result += k + '=' + encodeURIComponent(queryObj[k]) + '&'; } - } - - return result; -} +// aliases to formatQS +export let parseQueryStringParameters = internal.formatQS; // transform an AdServer targeting bids into a query string to send to the adserver export function transformAdServerTargetingObj(targeting) { @@ -291,10 +265,6 @@ function decorateLog(args, prefix) { return args; } -export function hasConsoleLogger() { - return consoleLogExists; -} - export function debugTurnedOn() { return !!config.getConfig('debug'); } @@ -322,53 +292,9 @@ export function createInvisibleIframe() { * and if it does return the value */ export function getParameterByName(name) { - var regexS = '[\\?&]' + name + '=([^&#]*)'; - var regex = new RegExp(regexS); - var results = regex.exec(window.location.search); - if (results === null) { - return ''; - } - - return decodeURIComponent(results[1].replace(/\+/g, ' ')); + return parseQS(getWindowLocation().search)[name] || ''; } -/** - * This function validates paramaters. - * @param {Object} paramObj [description] - * @param {string[]} requiredParamsArr [description] - * @return {boolean} Bool if paramaters are valid - */ -export function hasValidBidRequest(paramObj, requiredParamsArr, adapter) { - var found = false; - - function findParam(value, key) { - if (key === requiredParamsArr[i]) { - found = true; - } - } - - for (var i = 0; i < requiredParamsArr.length; i++) { - found = false; - - _each(paramObj, findParam); - - if (!found) { - logError('Params are missing for bid request. One of these required paramaters are missing: ' + requiredParamsArr, adapter); - return false; - } - } - - return true; -} - -// Handle addEventListener gracefully in older browsers -export function addEventHandler(element, event, func) { - if (element.addEventListener) { - element.addEventListener(event, func, true); - } else if (element.attachEvent) { - element.attachEvent('on' + event, func); - } -} /** * Return if the object is of the * given type. @@ -473,15 +399,6 @@ export function contains(a, obj) { return false; } -export let indexOf = (function () { - if (Array.prototype.indexOf) { - return Array.prototype.indexOf; - } - - // ie8 no longer supported - // return polyfills.indexOf; -}()); - /** * Map an array or object into another array * given a function @@ -649,32 +566,6 @@ export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') `; } -/** - * Returns iframe document in a browser agnostic way - * @param {Object} iframe reference - * @return {Object} iframe `document` reference - */ -export function getIframeDocument(iframe) { - if (!iframe) { - return; - } - - let doc; - try { - if (iframe.contentWindow) { - doc = iframe.contentWindow.document; - } else if (iframe.contentDocument.document) { - doc = iframe.contentDocument.document; - } else { - doc = iframe.contentDocument; - } - } catch (e) { - internal.logError('Cannot get iframe document', e); - } - - return doc; -} - export function getValueString(param, val, defaultValue) { if (val === undefined || val === null) { return defaultValue; @@ -794,16 +685,6 @@ export function adUnitsFilter(filter, bid) { return includes(filter, bid && bid.adUnitCode); } -/** - * Check if parent iframe of passed document supports content rendering via 'srcdoc' property - * @param {HTMLDocument} doc document to check support of 'srcdoc' - */ -export function isSrcdocSupported(doc) { - // Firefox is excluded due to https://bugzilla.mozilla.org/show_bug.cgi?id=1265961 - return doc.defaultView && doc.defaultView.frameElement && - 'srcdoc' in doc.defaultView.frameElement && !/firefox/i.test(navigator.userAgent); -} - export function deepClone(obj) { return clone(obj); } @@ -885,19 +766,6 @@ export function groupBy(xs, key) { }, {}); } -/** - * Returns content for a friendly iframe to execute a URL in script tag - * @param {string} url URL to be executed in a script tag in a friendly iframe - * and are macros left to be replaced if required - */ -export function createContentToExecuteExtScriptInFriendlyFrame(url) { - if (!url) { - return ''; - } - - return ``; -} - /** * Build an object consisting of only defined parameters to avoid creating an * object with defined keys and undefined values. @@ -1034,18 +902,6 @@ export function unsupportedBidderMessage(adUnit, bidder) { `; } -/** - * Delete property from object - * @param {Object} object - * @param {string} prop - * @return {Object} object - */ -export function deletePropertyFromObject(object, prop) { - let result = Object.assign({}, object); - delete result[prop]; - return result; -} - /** * Checks input is integer or not * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger @@ -1245,6 +1101,62 @@ export function compareOn(property) { } } +export function parseQS(query) { + return !query ? {} : query + .replace(/^\?/, '') + .split('&') + .reduce((acc, criteria) => { + let [k, v] = criteria.split('='); + if (/\[\]$/.test(k)) { + k = k.replace('[]', ''); + acc[k] = acc[k] || []; + acc[k].push(v); + } else { + acc[k] = v || ''; + } + return acc; + }, {}); +} + +export function formatQS(query) { + return Object + .keys(query) + .map(k => Array.isArray(query[k]) + ? query[k].map(v => `${k}[]=${v}`).join('&') + : `${k}=${query[k]}`) + .join('&'); +} + +export function parseUrl(url, options) { + let parsed = document.createElement('a'); + if (options && 'noDecodeWholeURL' in options && options.noDecodeWholeURL) { + parsed.href = url; + } else { + parsed.href = decodeURIComponent(url); + } + // in window.location 'search' is string, not object + let qsAsString = (options && 'decodeSearchAsString' in options && options.decodeSearchAsString); + return { + href: parsed.href, + protocol: (parsed.protocol || '').replace(/:$/, ''), + hostname: parsed.hostname, + port: +parsed.port, + pathname: parsed.pathname.replace(/^(?!\/)/, '/'), + search: (qsAsString) ? parsed.search : internal.parseQS(parsed.search || ''), + hash: (parsed.hash || '').replace(/^#/, ''), + host: parsed.host || window.location.host + }; +} + +export function buildUrl(obj) { + return (obj.protocol || 'http') + '://' + + (obj.host || + obj.hostname + (obj.port ? `:${obj.port}` : '')) + + (obj.pathname || '') + + (obj.search ? `?${internal.formatQS(obj.search || '')}` : '') + + (obj.hash ? `#${obj.hash}` : ''); +} + /** * This function compares two objects for checking their equivalence. * @param {Object} obj1 diff --git a/test/spec/modules/adformBidAdapter_spec.js b/test/spec/modules/adformBidAdapter_spec.js index 8b7ecb3f9a7..9233ca1dd7a 100644 --- a/test/spec/modules/adformBidAdapter_spec.js +++ b/test/spec/modules/adformBidAdapter_spec.js @@ -1,5 +1,4 @@ import {assert, expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/adformBidAdapter.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; diff --git a/test/spec/modules/adformOpenRTBBidAdapter_spec.js b/test/spec/modules/adformOpenRTBBidAdapter_spec.js index caa2fc56208..77dbc17cdb2 100644 --- a/test/spec/modules/adformOpenRTBBidAdapter_spec.js +++ b/test/spec/modules/adformOpenRTBBidAdapter_spec.js @@ -1,6 +1,5 @@ // jshint esversion: 6, es3: false, node: true import {assert, expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/adformOpenRTBBidAdapter.js'; import { NATIVE } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index 3ec6a19d20b..306914960c3 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -1,7 +1,6 @@ import {expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/adxcgBidAdapter.js'; -import {deepClone} from '../../../src/utils.js'; +import {deepClone, parseUrl} from 'src/utils.js'; describe('AdxcgAdapter', function () { let bidBanner = { @@ -119,7 +118,7 @@ describe('AdxcgAdapter', function () { let request = spec.buildRequests([bidBanner]); expect(request).to.exist; expect(request.method).to.equal('GET'); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); expect(parsedRequestUrl.hostname).to.equal('hbps.adxcg.net'); expect(parsedRequestUrl.pathname).to.equal('/get/adi'); @@ -152,7 +151,7 @@ describe('AdxcgAdapter', function () { let request = spec.buildRequests([bidVideo]); expect(request).to.exist; expect(request.method).to.equal('GET'); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); expect(parsedRequestUrl.hostname).to.equal('hbps.adxcg.net'); expect(parsedRequestUrl.pathname).to.equal('/get/adi'); @@ -191,7 +190,7 @@ describe('AdxcgAdapter', function () { let request = spec.buildRequests([bidNative]); expect(request).to.exist; expect(request.method).to.equal('GET'); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); expect(parsedRequestUrl.hostname).to.equal('hbps.adxcg.net'); expect(parsedRequestUrl.pathname).to.equal('/get/adi'); @@ -229,7 +228,7 @@ describe('AdxcgAdapter', function () { consentString: 'consentDataString' } }); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.gdpr).to.equal('1'); @@ -243,7 +242,7 @@ describe('AdxcgAdapter', function () { consentString: 'consentDataString' } }); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.gdpr).to.be.undefined; @@ -258,7 +257,7 @@ describe('AdxcgAdapter', function () { it('should send pubcid if available', function () { let request = spec.buildRequests(bid, bidderRequests); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.pubcid).to.equal('pubcidabcd'); }); @@ -272,7 +271,7 @@ describe('AdxcgAdapter', function () { it('should send pubcid if available', function () { let request = spec.buildRequests(bid, bidderRequests); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.tdid).to.equal('tdidabcd'); }); @@ -286,7 +285,7 @@ describe('AdxcgAdapter', function () { it('should send pubcid if available', function () { let request = spec.buildRequests(bid, bidderRequests); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.id5id).to.equal('id5idsample'); }); @@ -300,7 +299,7 @@ describe('AdxcgAdapter', function () { it('should send pubcid if available', function () { let request = spec.buildRequests(bid, bidderRequests); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.idl_env).to.equal('idl_envsample'); }); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index 0dd6b5ad065..3573681dd17 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import { parse } from '../../../src/url.js'; import { spec } from 'modules/adyoulikeBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index 7879ed37dc0..f7dcc1305e5 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { spec, VIDEO_ENDPOINT, BANNER_ENDPOINT, OUTSTREAM_SRC, DEFAULT_MIMES } from 'modules/beachfrontBidAdapter.js'; -import { parse as parseUrl } from 'src/url.js'; +import { parseUrl } from 'src/utils.js'; describe('BeachfrontAdapter', function () { let bidRequests; diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index 74caef69ccc..aa5807da0da 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -1,7 +1,6 @@ import { criteoIdSubmodule, storage } from 'modules/criteoIdSystem.js'; import * as utils from 'src/utils.js'; import * as ajaxLib from 'src/ajax.js'; -import * as urlLib from 'src/url.js'; const pastDateString = new Date(0).toString() @@ -34,7 +33,7 @@ describe('CriteoId module', function () { removeFromLocalStorageStub = sinon.stub(storage, 'removeDataFromLocalStorage'); timeStampStub = sinon.stub(utils, 'timestamp').returns(nowTimestamp); ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(mockResponse('{}')); - parseUrlStub = sinon.stub(urlLib, 'parse').returns({protocol: 'https', hostname: 'testdev.com'}) + parseUrlStub = sinon.stub(utils, 'parseUrl').returns({protocol: 'https', hostname: 'testdev.com'}) triggerPixelStub = sinon.stub(utils, 'triggerPixel'); done(); }); diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 562c7eb297b..895416bae2b 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -2,7 +2,6 @@ import { expect } from 'chai'; import parse from 'url-parse'; import { buildDfpVideoUrl, buildAdpodVideoUrl } from 'modules/dfpAdServerVideo.js'; -import { parseQS } from 'src/url.js'; import adUnit from 'test/fixtures/video/adUnit.json'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; @@ -33,7 +32,7 @@ describe('The DFP video support module', function () { expect(url.protocol).to.equal('https:'); expect(url.host).to.equal('securepubads.g.doubleclick.net'); - const queryParams = parseQS(url.query); + const queryParams = utils.parseQS(url.query); expect(queryParams).to.have.property('correlator'); expect(queryParams).to.have.property('description_url', 'someUrl.com'); expect(queryParams).to.have.property('env', 'vp'); @@ -57,7 +56,7 @@ describe('The DFP video support module', function () { expect(url.host).to.equal('video.adserver.example'); - const queryObject = parseQS(url.query); + const queryObject = utils.parseQS(url.query); expect(queryObject.description_url).to.equal('vastUrl.example'); }); @@ -78,7 +77,7 @@ describe('The DFP video support module', function () { params: { iu: 'my/adUnit' } })); - const queryObject = parseQS(url.query); + const queryObject = utils.parseQS(url.query); expect(queryObject.iu).to.equal('my/adUnit'); }); @@ -92,7 +91,7 @@ describe('The DFP video support module', function () { } })); - expect(parseQS(url.query)).to.have.property('output', 'vast'); + expect(utils.parseQS(url.query)).to.have.property('output', 'vast'); }); it('should include the cache key and adserver targeting in cust_params', function () { @@ -108,8 +107,8 @@ describe('The DFP video support module', function () { 'iu': 'my/adUnit' } })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('hb_uuid', bid.videoCacheKey); @@ -178,8 +177,8 @@ describe('The DFP video support module', function () { 'iu': 'my/adUnit' } })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('hb_uuid', bid.videoCacheKey); @@ -205,8 +204,8 @@ describe('The DFP video support module', function () { }, }, })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('my_targeting', 'foo'); @@ -224,8 +223,8 @@ describe('The DFP video support module', function () { url: 'https://video.adserver.example/ads?sz=640x480&iu=/123/aduniturl&impl=s&cust_params=section%3dblog%26mykey%3dmyvalue' })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('section', 'blog'); @@ -247,7 +246,7 @@ describe('The DFP video support module', function () { } })); - const queryObject = parseQS(url.query); + const queryObject = utils.parseQS(url.query); expect(queryObject.description_url).to.equal('descriptionurl.example'); }); @@ -272,8 +271,8 @@ describe('The DFP video support module', function () { 'iu': 'my/adUnit' } })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_uuid', bid.videoCacheKey); expect(customParams).to.have.property('hb_cache_id', bid.videoCacheKey); @@ -293,8 +292,8 @@ describe('The DFP video support module', function () { 'iu': 'my/adUnit' } })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_uuid', 'def'); expect(customParams).to.have.property('hb_cache_id', 'def'); @@ -370,7 +369,7 @@ describe('The DFP video support module', function () { expect(url.protocol).to.equal('https:'); expect(url.host).to.equal('securepubads.g.doubleclick.net'); - const queryParams = parseQS(url.query); + const queryParams = utils.parseQS(url.query); expect(queryParams).to.have.property('correlator'); expect(queryParams).to.have.property('description_url', 'someUrl.com'); expect(queryParams).to.have.property('env', 'vp'); @@ -382,7 +381,7 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('url'); expect(queryParams).to.have.property('cust_params'); - const custParams = parseQS(decodeURIComponent(queryParams.cust_params)); + const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); expect(custParams).to.have.property('hb_cache_id', '123'); expect(custParams).to.have.property('hb_pb_cat_dur', '15.00_395_15s,15.00_406_30s,10.00_395_15s'); } @@ -424,7 +423,7 @@ describe('The DFP video support module', function () { expect(url.protocol).to.equal('https:'); expect(url.host).to.equal('securepubads.g.doubleclick.net'); - const queryParams = parseQS(url.query); + const queryParams = utils.parseQS(url.query); expect(queryParams).to.have.property('correlator'); expect(queryParams).to.have.property('description_url', 'someUrl.com'); expect(queryParams).to.have.property('env', 'vp'); @@ -436,7 +435,7 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('url'); expect(queryParams).to.have.property('cust_params'); - const custParams = parseQS(decodeURIComponent(queryParams.cust_params)); + const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); expect(custParams).to.have.property('hb_cache_id', '123'); expect(custParams).to.have.property('hb_pb_cat_dur', '10.00_15s,15.00_15s,15.00_30s'); } diff --git a/test/spec/modules/emoteevBidAdapter_spec.js b/test/spec/modules/emoteevBidAdapter_spec.js index bf5bcc9e063..43ae62f1eb9 100644 --- a/test/spec/modules/emoteevBidAdapter_spec.js +++ b/test/spec/modules/emoteevBidAdapter_spec.js @@ -49,7 +49,6 @@ import { WALLPAPER, storage } from 'modules/emoteevBidAdapter.js'; -import * as url from '../../../src/url.js'; import * as utils from '../../../src/utils.js'; import {config} from '../../../src/config.js'; @@ -330,108 +329,108 @@ describe('emoteevBidAdapter', function () { }); describe('eventsUrl', function () { - expect(eventsUrl(null)).to.deep.equal(url.format({ + expect(eventsUrl(null)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: EVENTS_PATH })); - expect(eventsUrl('anything')).to.deep.equal(url.format({ + expect(eventsUrl('anything')).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: EVENTS_PATH })); - expect(eventsUrl(PRODUCTION)).to.deep.equal(url.format({ + expect(eventsUrl(PRODUCTION)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(PRODUCTION), pathname: EVENTS_PATH })); - expect(eventsUrl(STAGING)).to.deep.equal(url.format({ + expect(eventsUrl(STAGING)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(STAGING), pathname: EVENTS_PATH })); - expect(eventsUrl(DEVELOPMENT)).to.deep.equal(url.format({ + expect(eventsUrl(DEVELOPMENT)).to.deep.equal(utils.buildUrl({ hostname: domain(DEVELOPMENT), pathname: EVENTS_PATH })); }); describe('bidderUrl', function () { - expect(bidderUrl(null)).to.deep.equal(url.format({ + expect(bidderUrl(null)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: BIDDER_PATH })); - expect(bidderUrl('anything')).to.deep.equal(url.format({ + expect(bidderUrl('anything')).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: BIDDER_PATH })); - expect(bidderUrl(PRODUCTION)).to.deep.equal(url.format({ + expect(bidderUrl(PRODUCTION)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(PRODUCTION), pathname: BIDDER_PATH })); - expect(bidderUrl(STAGING)).to.deep.equal(url.format({ + expect(bidderUrl(STAGING)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(STAGING), pathname: BIDDER_PATH })); - expect(bidderUrl(DEVELOPMENT)).to.deep.equal(url.format({ + expect(bidderUrl(DEVELOPMENT)).to.deep.equal(utils.buildUrl({ hostname: domain(DEVELOPMENT), pathname: BIDDER_PATH })); }); describe('userSyncIframeUrl', function () { - expect(userSyncIframeUrl(null)).to.deep.equal(url.format({ + expect(userSyncIframeUrl(null)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: USER_SYNC_IFRAME_PATH })); - expect(userSyncIframeUrl('anything')).to.deep.equal(url.format({ + expect(userSyncIframeUrl('anything')).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: USER_SYNC_IFRAME_PATH })); - expect(userSyncIframeUrl(PRODUCTION)).to.deep.equal(url.format({ + expect(userSyncIframeUrl(PRODUCTION)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(PRODUCTION), pathname: USER_SYNC_IFRAME_PATH })); - expect(userSyncIframeUrl(STAGING)).to.deep.equal(url.format({ + expect(userSyncIframeUrl(STAGING)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(STAGING), pathname: USER_SYNC_IFRAME_PATH })); - expect(userSyncIframeUrl(DEVELOPMENT)).to.deep.equal(url.format({ + expect(userSyncIframeUrl(DEVELOPMENT)).to.deep.equal(utils.buildUrl({ hostname: domain(DEVELOPMENT), pathname: USER_SYNC_IFRAME_PATH })); }); describe('userSyncImageUrl', function () { - expect(userSyncImageUrl(null)).to.deep.equal(url.format({ + expect(userSyncImageUrl(null)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: USER_SYNC_IMAGE_PATH })); - expect(userSyncImageUrl('anything')).to.deep.equal(url.format({ + expect(userSyncImageUrl('anything')).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: USER_SYNC_IMAGE_PATH })); - expect(userSyncImageUrl(PRODUCTION)).to.deep.equal(url.format({ + expect(userSyncImageUrl(PRODUCTION)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(PRODUCTION), pathname: USER_SYNC_IMAGE_PATH })); - expect(userSyncImageUrl(STAGING)).to.deep.equal(url.format({ + expect(userSyncImageUrl(STAGING)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(STAGING), pathname: USER_SYNC_IMAGE_PATH })); - expect(userSyncImageUrl(DEVELOPMENT)).to.deep.equal(url.format({ + expect(userSyncImageUrl(DEVELOPMENT)).to.deep.equal(utils.buildUrl({ hostname: domain(DEVELOPMENT), pathname: USER_SYNC_IMAGE_PATH })); diff --git a/test/spec/modules/eplanningAnalyticsAdapter_spec.js b/test/spec/modules/eplanningAnalyticsAdapter_spec.js index e900f7ca31c..9bb71e7033d 100644 --- a/test/spec/modules/eplanningAnalyticsAdapter_spec.js +++ b/test/spec/modules/eplanningAnalyticsAdapter_spec.js @@ -1,7 +1,7 @@ import eplAnalyticsAdapter from 'modules/eplanningAnalyticsAdapter.js'; import includes from 'core-js/library/fn/array/includes.js'; import { expect } from 'chai'; -import {parse as parseURL} from 'src/url.js'; +import { parseUrl } from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; let adapterManager = require('src/adapterManager').default; let events = require('src/events'); @@ -118,7 +118,7 @@ describe('eplanning analytics adapter', function () { expect(includes(['https://ads.ar.e-planning.net/hba/1/12345?d='], requests[0].url)); let info = requests[0].url; - let purl = parseURL(info); + let purl = parseUrl(info); let eplData = JSON.parse(decodeURIComponent(purl.search.d)); // Step 8 check that 6 events were sent diff --git a/test/spec/modules/etargetBidAdapter_spec.js b/test/spec/modules/etargetBidAdapter_spec.js index 0d8ad7bcf11..4f5e0c224ec 100644 --- a/test/spec/modules/etargetBidAdapter_spec.js +++ b/test/spec/modules/etargetBidAdapter_spec.js @@ -1,5 +1,4 @@ import {assert, expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/etargetBidAdapter.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; diff --git a/test/spec/modules/fintezaAnalyticsAdapter_spec.js b/test/spec/modules/fintezaAnalyticsAdapter_spec.js index 4b58761d8a6..ae9ceceecc3 100644 --- a/test/spec/modules/fintezaAnalyticsAdapter_spec.js +++ b/test/spec/modules/fintezaAnalyticsAdapter_spec.js @@ -1,7 +1,7 @@ import fntzAnalyticsAdapter from 'modules/fintezaAnalyticsAdapter.js'; import includes from 'core-js/library/fn/array/includes.js'; import { expect } from 'chai'; -import { parse as parseURL } from 'src/url.js'; +import { parseUrl } from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; let adapterManager = require('src/adapterManager').default; @@ -81,7 +81,7 @@ describe('finteza analytics adapter', function () { expect(server.requests[0].method).to.equal('GET'); expect(server.requests[0].withCredentials).to.equal(true); - const url = parseURL(server.requests[0].url); + const url = parseUrl(server.requests[0].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -124,7 +124,7 @@ describe('finteza analytics adapter', function () { expect(server.requests[0].method).to.equal('GET'); expect(server.requests[0].withCredentials).to.equal(true); - let url = parseURL(server.requests[0].url); + let url = parseUrl(server.requests[0].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -138,7 +138,7 @@ describe('finteza analytics adapter', function () { expect(server.requests[1].method).to.equal('GET'); expect(server.requests[1].withCredentials).to.equal(true); - url = parseURL(server.requests[1].url); + url = parseUrl(server.requests[1].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -178,7 +178,7 @@ describe('finteza analytics adapter', function () { expect(server.requests[0].method).to.equal('GET'); expect(server.requests[0].withCredentials).to.equal(true); - const url = parseURL(server.requests[0].url); + const url = parseUrl(server.requests[0].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -217,7 +217,7 @@ describe('finteza analytics adapter', function () { expect(server.requests[0].method).to.equal('GET'); expect(server.requests[0].withCredentials).to.equal(true); - const url = parseURL(server.requests[0].url); + const url = parseUrl(server.requests[0].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); diff --git a/test/spec/modules/konduitWrapper_spec.js b/test/spec/modules/konduitWrapper_spec.js index bfae5711d32..4a0c627e885 100644 --- a/test/spec/modules/konduitWrapper_spec.js +++ b/test/spec/modules/konduitWrapper_spec.js @@ -2,7 +2,7 @@ import { expect } from 'chai'; import parse from 'url-parse'; import { buildVastUrl } from 'modules/konduitWrapper.js'; -import { parseQS } from 'src/url.js'; +import { parseQS } from 'src/utils.js'; import { config } from 'src/config.js'; describe('The Konduit vast wrapper module', function () { diff --git a/test/spec/modules/mgidBidAdapter_spec.js b/test/spec/modules/mgidBidAdapter_spec.js index b96d718ab36..16f4f0b4607 100644 --- a/test/spec/modules/mgidBidAdapter_spec.js +++ b/test/spec/modules/mgidBidAdapter_spec.js @@ -1,7 +1,6 @@ import {assert, expect} from 'chai'; import {spec} from 'modules/mgidBidAdapter.js'; import * as utils from '../../../src/utils.js'; -import * as urlUtils from '../../../src/url.js'; describe('Mgid bid adapter', function () { let sandbox; @@ -361,7 +360,7 @@ describe('Mgid bid adapter', function () { }; let bidRequests = [bid]; const page = top.location.href; - const domain = urlUtils.parse(page).hostname; + const domain = utils.parseUrl(page).hostname; const request = spec.buildRequests(bidRequests); expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); expect(request.method).deep.equal('POST'); @@ -409,7 +408,7 @@ describe('Mgid bid adapter', function () { let bidRequests = [bid]; const page = top.location.href; - const domain = urlUtils.parse(page).hostname; + const domain = utils.parseUrl(page).hostname; const request = spec.buildRequests(bidRequests); expect(request).to.be.a('object'); expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); @@ -446,7 +445,7 @@ describe('Mgid bid adapter', function () { let bidRequests = [bid]; const page = top.location.href; - const domain = urlUtils.parse(page).hostname; + const domain = utils.parseUrl(page).hostname; const request = spec.buildRequests(bidRequests); expect(request).to.be.a('object'); expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); @@ -482,7 +481,7 @@ describe('Mgid bid adapter', function () { let bidRequests = [bid]; const page = top.location.href; - const domain = urlUtils.parse(page).hostname; + const domain = utils.parseUrl(page).hostname; const request = spec.buildRequests(bidRequests); expect(request).to.be.a('object'); expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); @@ -516,7 +515,7 @@ describe('Mgid bid adapter', function () { const request = spec.buildRequests(bidRequests); const page = top.location.href; - const domain = urlUtils.parse(page).hostname; + const domain = utils.parseUrl(page).hostname; expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); expect(request.method).deep.equal('POST'); const data = JSON.parse(request.data); diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index 351cea0b086..5e4093c91be 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -10,7 +10,7 @@ import { spec as qcSpec } from '../../../modules/quantcastBidAdapter.js'; import { newBidder } from '../../../src/adapters/bidderFactory.js'; -import { parse } from 'src/url.js'; +import { parseUrl } from 'src/utils.js'; import { config } from 'src/config.js'; describe('Quantcast adapter', function () { @@ -90,13 +90,13 @@ describe('Quantcast adapter', function () { describe('`buildRequests`', function () { it('sends secure bid requests', function () { const requests = qcSpec.buildRequests([bidRequest]); - const url = parse(requests[0]['url']); + const url = parseUrl(requests[0]['url']); expect(url.protocol).to.equal('https'); }); it('sends bid requests to Quantcast Canary Endpoint if `publisherId` is `test-publisher`', function () { const requests = qcSpec.buildRequests([bidRequest]); - const url = parse(requests[0]['url']); + const url = parseUrl(requests[0]['url']); expect(url.hostname).to.equal(QUANTCAST_TEST_DOMAIN); }); diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js index a0cff8a3464..eb9077fac39 100644 --- a/test/spec/modules/readpeakBidAdapter_spec.js +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { spec, ENDPOINT } from 'modules/readpeakBidAdapter.js'; import { config } from 'src/config.js'; -import { parse as parseUrl } from 'src/url.js'; +import { parseUrl } from 'src/utils.js'; describe('ReadPeakAdapter', function() { let bidRequest; diff --git a/test/spec/modules/revcontentBidAdapter_spec.js b/test/spec/modules/revcontentBidAdapter_spec.js index 548f3019a4f..1aa08d9469e 100644 --- a/test/spec/modules/revcontentBidAdapter_spec.js +++ b/test/spec/modules/revcontentBidAdapter_spec.js @@ -1,6 +1,5 @@ // jshint esversion: 6, es3: false, node: true import {assert, expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/revcontentBidAdapter.js'; import { NATIVE } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; diff --git a/test/spec/modules/seedingAllianceAdapter_spec.js b/test/spec/modules/seedingAllianceAdapter_spec.js index c5c2a5d682e..e6f96c92fd9 100755 --- a/test/spec/modules/seedingAllianceAdapter_spec.js +++ b/test/spec/modules/seedingAllianceAdapter_spec.js @@ -1,6 +1,5 @@ // jshint esversion: 6, es3: false, node: true import {assert, expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/seedingAllianceBidAdapter.js'; import { NATIVE } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index ccca9bdd0dc..6d0595ba4d8 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -247,7 +247,7 @@ describe('bidders created by newBidder', function () { bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2&`); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); expect(ajaxStub.firstCall.args[2]).to.be.undefined; expect(ajaxStub.firstCall.args[3]).to.deep.equal({ method: 'GET', @@ -271,7 +271,7 @@ describe('bidders created by newBidder', function () { bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2&`); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); expect(ajaxStub.firstCall.args[2]).to.be.undefined; expect(ajaxStub.firstCall.args[3]).to.deep.equal({ method: 'GET', diff --git a/test/spec/url_spec.js b/test/spec/url_spec.js deleted file mode 100644 index 842b954c4bc..00000000000 --- a/test/spec/url_spec.js +++ /dev/null @@ -1,94 +0,0 @@ -import {format, parse} from '../../src/url.js'; -import { expect } from 'chai'; - -describe('helpers.url', function () { - describe('parse()', function () { - let parsed; - - beforeEach(function () { - parsed = parse('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash'); - }); - - it('extracts the protocol', function () { - expect(parsed).to.have.property('protocol', 'http'); - }); - - it('extracts the hostname', function () { - expect(parsed).to.have.property('hostname', 'example.com'); - }); - - it('extracts the port', function () { - expect(parsed).to.have.property('port', 3000); - }); - - it('extracts the pathname', function () { - expect(parsed).to.have.property('pathname', '/pathname/'); - }); - - it('extracts the search query', function () { - expect(parsed).to.have.property('search'); - expect(parsed.search).to.eql({ - foo: 'xxx', - search: 'test', - bar: 'foo', - }); - }); - - it('extracts the hash', function () { - expect(parsed).to.have.property('hash', 'hash'); - }); - - it('extracts the host', function () { - expect(parsed).to.have.property('host', 'example.com:3000'); - }); - }); - - describe('parse(url, {noDecodeWholeURL: true})', function () { - let parsed; - - beforeEach(function () { - parsed = parse('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash', {noDecodeWholeURL: true}); - }); - - it('extracts the search query', function () { - expect(parsed).to.have.property('search'); - expect(parsed.search).to.eql({ - foo: 'bar', - search: 'test', - bar: 'foo%26foo%3Dxxx', - }); - }); - }); - - describe('format()', function () { - it('formats an object in to a URL', function () { - expect(format({ - protocol: 'http', - hostname: 'example.com', - port: 3000, - pathname: '/pathname/', - search: {foo: 'bar', search: 'test', bar: 'foo%26foo%3Dxxx'}, - hash: 'hash' - })).to.equal('http://example.com:3000/pathname/?foo=bar&search=test&bar=foo%26foo%3Dxxx#hash'); - }); - - it('will use defaults for missing properties', function () { - expect(format({ - hostname: 'example.com' - })).to.equal('http://example.com'); - }); - }); - - describe('parse(url, {decodeSearchAsString: true})', function () { - let parsed; - - beforeEach(function () { - parsed = parse('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash', {decodeSearchAsString: true}); - }); - - it('extracts the search query', function () { - expect(parsed).to.have.property('search'); - expect(parsed.search).to.equal('?search=test&foo=bar&bar=foo&foo=xxx'); - }); - }); -}); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js old mode 100755 new mode 100644 index 9ba70810a08..2c29877f724 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -18,24 +18,6 @@ describe('Utils', function () { type_array = 'Array', type_function = 'Function'; - describe('replaceTokenInString', function () { - it('should replace all given tokens in a String', function () { - var tokensToReplace = { - foo: 'bar', - zap: 'quux' - }; - - var output = utils.replaceTokenInString('hello %FOO%, I am %ZAP%', tokensToReplace, '%'); - assert.equal(output, 'hello bar, I am quux'); - }); - - it('should ignore tokens it does not see', function () { - var output = utils.replaceTokenInString('hello %FOO%', {}, '%'); - - assert.equal(output, 'hello %FOO%'); - }); - }); - describe('getBidIdParameter', function () { it('should return value of the key in input object', function () { var obj = { @@ -86,7 +68,7 @@ describe('Utils', function () { }; var output = utils.parseQueryStringParameters(obj); - var expectedResult = 'a=' + encodeURIComponent('1') + '&b=' + encodeURIComponent('2') + '&'; + var expectedResult = 'a=' + encodeURIComponent('1') + '&b=' + encodeURIComponent('2'); assert.equal(output, expectedResult); }); @@ -699,20 +681,6 @@ describe('Utils', function () { }); }); - describe('createContentToExecuteExtScriptInFriendlyFrame', function () { - it('should return empty string if url is not passed', function () { - var output = utils.createContentToExecuteExtScriptInFriendlyFrame(); - assert.equal(output, ''); - }); - - it('should have URL in returned value if url is passed', function () { - var url = 'https://abcd.com/service?a=1&b=2&c=3'; - var output = utils.createContentToExecuteExtScriptInFriendlyFrame(url); - var expected = ``; - assert.equal(output, expected); - }); - }); - describe('getDefinedParams', function () { it('builds an object consisting of defined params', function () { const adUnit = { @@ -833,6 +801,98 @@ describe('Utils', function () { }); }); + describe('URL helpers', function () { + describe('parseUrl()', function () { + let parsed; + + beforeEach(function () { + parsed = utils.parseUrl('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash'); + }); + + it('extracts the protocol', function () { + expect(parsed).to.have.property('protocol', 'http'); + }); + + it('extracts the hostname', function () { + expect(parsed).to.have.property('hostname', 'example.com'); + }); + + it('extracts the port', function () { + expect(parsed).to.have.property('port', 3000); + }); + + it('extracts the pathname', function () { + expect(parsed).to.have.property('pathname', '/pathname/'); + }); + + it('extracts the search query', function () { + expect(parsed).to.have.property('search'); + expect(parsed.search).to.eql({ + foo: 'xxx', + search: 'test', + bar: 'foo', + }); + }); + + it('extracts the hash', function () { + expect(parsed).to.have.property('hash', 'hash'); + }); + + it('extracts the host', function () { + expect(parsed).to.have.property('host', 'example.com:3000'); + }); + }); + + describe('parseUrl(url, {noDecodeWholeURL: true})', function () { + let parsed; + + beforeEach(function () { + parsed = utils.parseUrl('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash', {noDecodeWholeURL: true}); + }); + + it('extracts the search query', function () { + expect(parsed).to.have.property('search'); + expect(parsed.search).to.eql({ + foo: 'bar', + search: 'test', + bar: 'foo%26foo%3Dxxx', + }); + }); + }); + + describe('buildUrl()', function () { + it('formats an object in to a URL', function () { + expect(utils.buildUrl({ + protocol: 'http', + hostname: 'example.com', + port: 3000, + pathname: '/pathname/', + search: {foo: 'bar', search: 'test', bar: 'foo%26foo%3Dxxx'}, + hash: 'hash' + })).to.equal('http://example.com:3000/pathname/?foo=bar&search=test&bar=foo%26foo%3Dxxx#hash'); + }); + + it('will use defaults for missing properties', function () { + expect(utils.buildUrl({ + hostname: 'example.com' + })).to.equal('http://example.com'); + }); + }); + + describe('parseUrl(url, {decodeSearchAsString: true})', function () { + let parsed; + + beforeEach(function () { + parsed = utils.parseUrl('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash', {decodeSearchAsString: true}); + }); + + it('extracts the search query', function () { + expect(parsed).to.have.property('search'); + expect(parsed.search).to.equal('?search=test&foo=bar&bar=foo&foo=xxx'); + }); + }); + }); + describe('transformBidderParamKeywords', function () { it('returns an array of objects when keyvalue is an array', function () { let keywords = { From 8f8f6f8d69ff0ea2cd340a03d1c562ddd1888497 Mon Sep 17 00:00:00 2001 From: vincentproxistore <56686565+vincentproxistore@users.noreply.github.com> Date: Fri, 10 Apr 2020 15:34:43 +0200 Subject: [PATCH 85/90] change payload (#5105) --- modules/proxistoreBidAdapter.js | 19 ++++++++----------- .../spec/modules/proxistoreBidAdapter_spec.js | 12 ++++++++---- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/modules/proxistoreBidAdapter.js b/modules/proxistoreBidAdapter.js index 6facf5d1480..e8ef4878d8e 100644 --- a/modules/proxistoreBidAdapter.js +++ b/modules/proxistoreBidAdapter.js @@ -2,16 +2,16 @@ const { registerBidder } = require('../src/adapters/bidderFactory.js'); const BIDDER_CODE = 'proxistore'; const PROXISTORE_VENDOR_ID = 418; -function _mapSizes(sizes) { - const flatSize = sizes.reduce((acc, val) => acc.concat(val), []); ; - return flatSize.map(array1d => { return { width: array1d[0], height: array1d[1] } }); -} - function _createServerRequest(bidRequests, bidderRequest) { + const sizeIds = []; + bidRequests.forEach(bid => { + const sizeId = {id: bid.bidId, sizes: bid.sizes.map(size => { return { width: size[0], height: size[1] } })}; + sizeIds.push(sizeId); + }); const payload = { auctionId: bidRequests[0].auctionId, - transactionId: bidRequests[0].transactionId, - sizes: _mapSizes(bidRequests.map(x => x.sizes)), + transactionId: bidRequests[0].auctionId, + bids: sizeIds, website: bidRequests[0].params.website, language: bidRequests[0].params.language, gdpr: { @@ -19,9 +19,6 @@ function _createServerRequest(bidRequests, bidderRequest) { } }; - const bidIds = bidRequests.map(req => req.bidId); - bidIds.length === 1 ? payload.bidId = bidIds[0] : payload.bidIds = bidIds; - const options = { contentType: 'application/json', withCredentials: true @@ -42,7 +39,7 @@ function _createServerRequest(bidRequests, bidderRequest) { return { method: 'POST', - url: bidRequests[0].params.url || 'https://abs.proxistore.com/' + payload.language + '/v3/rtb/prebid', + url: bidRequests[0].params.url || 'https://abs.proxistore.com/' + payload.language + '/v3/rtb/prebid/multi', data: JSON.stringify(payload), options: options }; diff --git a/test/spec/modules/proxistoreBidAdapter_spec.js b/test/spec/modules/proxistoreBidAdapter_spec.js index 579ef5fdb3b..e18262ae797 100644 --- a/test/spec/modules/proxistoreBidAdapter_spec.js +++ b/test/spec/modules/proxistoreBidAdapter_spec.js @@ -34,7 +34,7 @@ describe('ProxistoreBidAdapter', function () { }); describe('buildRequests', function () { - const url = 'https://abs.proxistore.com/fr/v3/rtb/prebid'; + const url = 'https://abs.proxistore.com/fr/v3/rtb/prebid/multi'; const request = spec.buildRequests([bid], bidderRequest); it('should return a valid object', function () { expect(request).to.be.an('object'); @@ -55,10 +55,14 @@ describe('ProxistoreBidAdapter', function () { expect(data.gdpr.applies).to.be.true; expect(data.gdpr.consentGiven).to.be.true; }); - it('should have a property bidId if there is only one bid', function () { + it('should have a property a length of bids equal to one if there is only one bid', function () { const data = JSON.parse(request.data); - expect(data.hasOwnProperty('bidId')).to.be.true; - }) + expect(data.hasOwnProperty('bids')).to.be.true; + expect(data.bids).to.be.an('array'); + expect(data.bids.length).equal(1); + expect(data.bids[0].hasOwnProperty('id')).to.be.true; + expect(data.bids[0].sizes).to.be.an('array'); + }); }); describe('interpretResponse', function () { From 67a1cb134c784fdf99a60b3b349643e21267e557 Mon Sep 17 00:00:00 2001 From: rtuschkany <35923908+rtuschkany@users.noreply.github.com> Date: Fri, 10 Apr 2020 20:30:25 +0200 Subject: [PATCH 86/90] Add new bid adapter for ConnectAd (#4806) * Add new bid adapter for ConnectAd * Add new bid adapter for ConnectAd * Conditional fix * js.fix * js-fix * EOF-fix * EOF-Fix --- modules/connectadBidAdapter.js | 299 ++++++++++++ modules/connectadBidAdapter.md | 46 ++ test/spec/modules/connectadBidAdapter_spec.js | 453 ++++++++++++++++++ 3 files changed, 798 insertions(+) create mode 100644 modules/connectadBidAdapter.js create mode 100644 modules/connectadBidAdapter.md create mode 100644 test/spec/modules/connectadBidAdapter_spec.js diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js new file mode 100644 index 00000000000..3dcb8da9838 --- /dev/null +++ b/modules/connectadBidAdapter.js @@ -0,0 +1,299 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js' +import {config} from '../src/config.js'; + +const BIDDER_CODE = 'connectad'; +const BIDDER_CODE_ALIAS = 'connectadrealtime'; +const ENDPOINT_URL = 'https://i.connectad.io/api/v2'; +const SUPPORTED_MEDIA_TYPES = [BANNER]; + +export const spec = { + code: BIDDER_CODE, + aliases: [ BIDDER_CODE_ALIAS ], + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + + isBidRequestValid: function(bid) { + return !!(bid.params.networkId && bid.params.siteId); + }, + + buildRequests: function(validBidRequests, bidderRequest) { + let digitrust; + + let ret = { + method: 'POST', + url: '', + data: '', + bidRequest: [] + }; + + if (validBidRequests.length < 1) { + return ret; + } + + const data = Object.assign({ + placements: [], + time: Date.now(), + user: {}, + url: (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) ? bidderRequest.refererInfo.referer : window.location.href, + referrer: window.document.referrer, + referrer_info: bidderRequest.refererInfo, + screensize: getScreenSize(), + dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, + language: navigator.language, + ua: navigator.userAgent + }); + + // coppa compliance + if (config.getConfig('coppa') === true) { + utils.deepSetValue(data, 'user.coppa', 1); + } + + // adding schain object + if (validBidRequests[0].schain) { + utils.deepSetValue(data, 'source.ext.schain', validBidRequests[0].schain); + } + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + let gdprApplies; + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + gdprApplies = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + utils.deepSetValue(data, 'user.ext.gdpr', gdprApplies); + utils.deepSetValue(data, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + } + + // CCPA + if (bidderRequest.uspConsent) { + utils.deepSetValue(data, 'user.ext.us_privacy', bidderRequest.uspConsent); + } + + // Digitrust Support + const bidRequestDigitrust = utils.deepAccess(validBidRequests[0], 'userId.digitrustid.data'); + if (bidRequestDigitrust && (!bidRequestDigitrust.privacy || !bidRequestDigitrust.privacy.optout)) { + digitrust = { + id: bidRequestDigitrust.id, + keyv: bidRequestDigitrust.keyv + } + } + + if (digitrust) { + utils.deepSetValue(data, 'user.ext.digitrust', { + id: digitrust.id, + keyv: digitrust.keyv + }) + } + + if (validBidRequests[0].userId && typeof validBidRequests[0].userId === 'object' && (validBidRequests[0].userId.tdid || validBidRequests[0].userId.pubcid || validBidRequests[0].userId.lipb || validBidRequests[0].userId.id5id || validBidRequests[0].userId.parrableid)) { + utils.deepSetValue(data, 'user.ext.eids', []); + + if (validBidRequests[0].userId.tdid) { + data.user.ext.eids.push({ + source: 'adserver.org', + uids: [{ + id: validBidRequests[0].userId.tdid, + ext: { + rtiPartner: 'TDID' + } + }] + }); + } + + if (validBidRequests[0].userId.pubcid) { + data.user.ext.eids.push({ + source: 'pubcommon', + uids: [{ + id: validBidRequests[0].userId.pubcid, + }] + }); + } + + if (validBidRequests[0].userId.id5id) { + data.user.ext.eids.push({ + source: 'id5-sync.com', + uids: [{ + id: validBidRequests[0].userId.id5id, + }] + }); + } + + if (validBidRequests[0].userId.parrableid) { + data.user.ext.eids.push({ + source: 'parrable.com', + uids: [{ + id: validBidRequests[0].userId.parrableid, + }] + }); + } + + if (validBidRequests[0].userId.lipb && validBidRequests[0].userId.lipb.lipbid) { + data.user.ext.eids.push({ + source: 'liveintent.com', + uids: [{ + id: validBidRequests[0].userId.lipb.lipbid + }] + }); + } + } + + validBidRequests.map(bid => { + const placement = Object.assign({ + id: bid.transactionId, + divName: bid.bidId, + sizes: bid.mediaTypes.banner.sizes, + adTypes: getSize(bid.mediaTypes.banner.sizes || bid.sizes) + }, bid.params); + + if (placement.networkId && placement.siteId) { + data.placements.push(placement); + } + }); + + ret.data = JSON.stringify(data); + ret.bidRequest = validBidRequests; + ret.url = ENDPOINT_URL; + + return ret; + }, + + interpretResponse: function(serverResponse, bidRequest, bidderRequest) { + let bid; + let bids; + let bidId; + let bidObj; + let bidResponses = []; + + bids = bidRequest.bidRequest; + + serverResponse = (serverResponse || {}).body; + for (let i = 0; i < bids.length; i++) { + bid = {}; + bidObj = bids[i]; + bidId = bidObj.bidId; + + if (serverResponse) { + const decision = serverResponse.decisions && serverResponse.decisions[bidId]; + const price = decision && decision.pricing && decision.pricing.clearPrice; + + if (decision && price) { + bid.requestId = bidId; + bid.cpm = price; + bid.width = decision.width; + bid.height = decision.height; + bid.dealid = decision.dealid || null; + bid.ad = retrieveAd(decision); + bid.currency = 'USD'; + bid.creativeId = decision.adId; + bid.ttl = 360; + bid.netRevenue = true; + bidResponses.push(bid); + } + } + } + + return bidResponses; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncEndpoint = 'https://cdn.connectad.io/connectmyusers.php?'; + + if (gdprConsent) { + syncEndpoint = utils.tryAppendQueryString(syncEndpoint, 'gdpr', (gdprConsent.gdprApplies ? 1 : 0)); + } + + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + syncEndpoint = utils.tryAppendQueryString(syncEndpoint, 'gdpr_consent', gdprConsent.consentString); + } + + if (uspConsent) { + syncEndpoint = utils.tryAppendQueryString(syncEndpoint, 'us_privacy', uspConsent); + } + + if (config.getConfig('coppa') === true) { + syncEndpoint = utils.tryAppendQueryString(syncEndpoint, 'coppa', 1); + } + + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: syncEndpoint + }]; + } else { + utils.logWarn('Bidder ConnectAd: Please activate iFrame Sync'); + } + } +}; + +const sizeMap = [ + null, + '120x90', + '200x200', + '468x60', + '728x90', + '300x250', + '160x600', + '120x600', + '300x100', + '180x150', + '336x280', + '240x400', + '234x60', + '88x31', + '120x60', + '120x240', + '125x125', + '220x250', + '250x250', + '250x90', + '0x0', + '200x90', + '300x50', + '320x50', + '320x480', + '185x185', + '620x45', + '300x125', + '800x250', + '980x120', + '980x150', + '320x150', + '300x300', + '200x600', + '320x500', + '320x320' +]; + +sizeMap[77] = '970x90'; +sizeMap[123] = '970x250'; +sizeMap[43] = '300x600'; +sizeMap[286] = '970x66'; +sizeMap[3230] = '970x280'; +sizeMap[429] = '486x60'; +sizeMap[374] = '700x500'; +sizeMap[934] = '300x1050'; +sizeMap[1578] = '320x100'; +sizeMap[331] = '320x250'; +sizeMap[3301] = '320x267'; +sizeMap[2730] = '728x250'; + +function getSize(sizes) { + const result = []; + sizes.forEach(function(size) { + const index = sizeMap.indexOf(size[0] + 'x' + size[1]); + if (index >= 0) { + result.push(index); + } + }); + return result; +} + +function retrieveAd(decision) { + return decision.contents && decision.contents[0] && decision.contents[0].body; +} + +function getScreenSize() { + return [window.screen.width, window.screen.height].join('x'); +} + +registerBidder(spec); diff --git a/modules/connectadBidAdapter.md b/modules/connectadBidAdapter.md new file mode 100644 index 00000000000..e63494e1add --- /dev/null +++ b/modules/connectadBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: ConnectAd PreBid Adapter +Module Type: Bidder Adapter +Maintainer: support@connectad.io +``` + +# Description + +ConnectAd bid adapter supports only Banner at present. Video and Mobile will follow Q2/2020 + +# Sample Ad Unit: For Publishers +``` +var adUnits = [ +{ + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'connectad', + params: { + siteId: 123456, + networkId: 123456, + bidfloor: 0.20 // Optional: Requested Bidfloor + } + }] +} + +# ## Configuration +ConnectAd recommends the UserSync configuration below otherwise we will not be able to performe user syncs. + +```javascript +pbjs.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: ['connectad'], + filter: 'include' + } + } + } +}); \ No newline at end of file diff --git a/test/spec/modules/connectadBidAdapter_spec.js b/test/spec/modules/connectadBidAdapter_spec.js new file mode 100644 index 00000000000..626018241c4 --- /dev/null +++ b/test/spec/modules/connectadBidAdapter_spec.js @@ -0,0 +1,453 @@ +import {expect} from 'chai'; +import {spec} from 'modules/connectadBidAdapter.js'; +import { config } from 'src/config.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; + +describe('ConnectAd Adapter', function () { + let bidRequests; + let bidderRequest; + let bidRequestsUserIds; + + beforeEach(function () { + bidRequests = [ + { + bidder: 'conntectad', + params: { + siteId: 123456, + networkId: 123456 + }, + adUnitCode: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bidId: '2f95c00074b931', + auctionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df' + } + ]; + + bidRequestsUserIds = [{ + bidder: 'conntectad', + params: { + siteId: 123456, + networkId: 123456 + }, + adUnitCode: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bidId: '2f95c00074b931', + auctionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', + userId: { + tdid: '123456', + digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}} + } + }]; + + bidderRequest = { + timeout: 3000, + gdprConsent: { + gdprApplies: true, + consentString: 'consentDataString', + vendorData: {} + } + } + }); + + describe('inherited functions', function () { + it('should exists and is a function', function () { + const adapter = newBidder(spec); + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('implementation', function () { + describe('for requests', function () { + it('should accept bid', function () { + let validBid = { + bidder: 'connectad', + params: { + siteId: 123456, + networkId: 123456 + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + } + }; + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + + it('should reject if missing sizes', function () { + let invalidBid = { + bidder: 'connectad', + params: { + siteId: 123456, + } + }; + const isValid = spec.isBidRequestValid(invalidBid); + expect(isValid).to.equal(false); + }); + + it('should return true when optional bidFloor params found for an ad', function () { + let validBid = { + bidder: 'connectad', + params: { + siteId: 123456, + networkId: 123456, + bidfloor: 0.20 + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + } + }; + const isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true) + }); + + it('should reject if missing siteId/networkId', function () { + let invalidBid = { + bidder: 'connectad', + params: {}, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + } + }; + const isValid = spec.isBidRequestValid(invalidBid); + expect(isValid).to.equal(false); + }); + + it('should reject if missing networkId', function () { + let invalidBid = { + bidder: 'connectad', + params: { + siteId: 123456 + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + } + }; + const isValid = spec.isBidRequestValid(invalidBid); + expect(isValid).to.equal(false); + }); + + it('should contain SiteId and NetworkId', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.placements[0].siteId).to.equal(123456); + expect(requestparse.placements[0].networkId).to.equal(123456); + }); + + it('should contain gdpr info', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.user.ext.gdpr).to.equal(1); + expect(requestparse.user.ext.consent).to.equal('consentDataString'); + }); + + it('should build a request if Consent but no gdprApplies', function () { + let bidderRequest = { + timeout: 3000, + gdprConsent: { + gdprApplies: false, + consentString: 'consentDataString', + }, + } + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.placements[0].adTypes).to.be.an('array'); + expect(requestparse.placements[0].siteId).to.equal(123456); + expect(requestparse.user.ext.consent).to.equal('consentDataString'); + }); + + it('should build a request if gdprConsent empty', function () { + let bidderRequest = { + timeout: 3000, + gdprConsent: {} + } + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.placements[0].adTypes).to.be.an('array'); + expect(requestparse.placements[0].siteId).to.equal(123456); + }); + + it('should have CCPA Consent if defined', function () { + const uspConsent = '1YYN' + bidderRequest.uspConsent = uspConsent + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.user.ext.us_privacy).to.equal(uspConsent); + }); + + it('should not have CCPA Consent if not defined', function () { + bidderRequest.uspConsent = undefined + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.user.ext.us_privacy).to.be.undefined; + }); + + it('should not include schain when not provided', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.source).to.not.exist; + }); + + it('should submit coppa if set in config', function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.user.coppa).to.equal(1); + config.getConfig.restore(); + }); + + it('should send all UserData data', function () { + const request = spec.buildRequests(bidRequestsUserIds, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.user.ext.eids).to.be.an('array'); + expect(requestparse.user.ext.eids[0].uids[0].id).to.equal('123456'); + expect(requestparse.user.ext.digitrust.id).to.equal('DTID'); + }); + + it('should add referer info', function () { + const bidRequest = Object.assign({}, bidRequests[0]) + const bidderRequ = { + refererInfo: { + referer: 'https://connectad.io/page.html', + reachedTop: true, + numIframes: 2, + stack: [ + 'https://connectad.io/page.html', + 'https://connectad.io/iframe1.html', + 'https://connectad.io/iframe2.html' + ] + } + } + const request = spec.buildRequests([bidRequest], bidderRequ); + const requestparse = JSON.parse(request.data); + + expect(requestparse.referrer_info).to.exist; + }); + + it('should populate schain', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'reseller1.com', + 'sid': 'absc1', + 'hp': 1 + } + ] + } + }); + + const request = spec.buildRequests([bidRequest], bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.source.ext.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'reseller1.com', + 'sid': 'absc1', + 'hp': 1 + } + ] + }); + }); + }); + + describe('bid responses', function () { + it('should return complete bid response', function () { + let serverResponse = { + body: { + decisions: { + '2f95c00074b931': { + adId: '0', + contents: [ + { + body: '<<<---- Creative --->>>' + } + ], + height: '250', + width: '300', + pricing: { + clearPrice: 11.899999999999999 + } + } + } + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(1); + expect(bids[0].cpm).to.equal(11.899999999999999); + expect(bids[0].width).to.equal('300'); + expect(bids[0].height).to.equal('250'); + expect(bids[0].ad).to.have.length.above(1); + }); + + it('should return empty bid response', function () { + let serverResponse = { + body: { + decisions: [] + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response on incorrect size', function () { + let serverResponse = { + body: { + decisions: { + '2f95c00074b931': { + adId: '0', + contents: [ + { + body: '<<<---- Creative --->>>' + } + ], + height: '160', + width: '600', + pricing: { + clearPrice: 0 + } + } + } + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response on 0 cpm', function () { + let serverResponse = { + body: { + decisions: { + '2f95c00074b931': { + adId: '0', + contents: [ + { + body: '<<<---- Creative --->>>' + } + ], + height: '300', + width: '250', + pricing: { + clearPrice: 0 + } + } + } + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.lengthOf(0); + }); + + it('should process a deal id', function () { + let serverResponse = { + body: { + decisions: { + '2f95c00074b931': { + adId: '0', + dealid: 'ABC90210', + contents: [ + { + body: '<<<---- Creative --->>>' + } + ], + height: '300', + width: '250', + pricing: { + clearPrice: 11.899999999999999 + } + } + } + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.lengthOf(1); + expect(bids[0].dealid).to.equal('ABC90210'); + }); + }); + }); + + describe('getUserSyncs', () => { + let testParams = [ + { + name: 'iframe/no gdpr or ccpa', + arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, null], + expect: { + type: 'iframe', + pixels: ['https://cdn.connectad.io/connectmyusers.php?'] + } + }, + { + name: 'iframe/gdpr', + arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, {gdprApplies: true, consentString: '234234'}], + expect: { + type: 'iframe', + pixels: ['https://cdn.connectad.io/connectmyusers.php?gdpr=1&gdpr_consent=234234&'] + } + }, + { + name: 'iframe/ccpa', + arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, null, 'YN12'], + expect: { + type: 'iframe', + pixels: ['https://cdn.connectad.io/connectmyusers.php?us_privacy=YN12&'] + } + }, + { + name: 'iframe/ccpa & gdpr', + arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, {gdprApplies: true, consentString: '234234'}, 'YN12'], + expect: { + type: 'iframe', + pixels: ['https://cdn.connectad.io/connectmyusers.php?gdpr=1&gdpr_consent=234234&us_privacy=YN12&'] + } + } + ]; + + for (let i = 0; i < testParams.length; i++) { + let currParams = testParams[i]; + it(currParams.name, function () { + const result = spec.getUserSyncs.apply(this, currParams.arguments); + expect(result).to.have.lengthOf(currParams.expect.pixels.length); + for (let ix = 0; ix < currParams.expect.pixels.length; ix++) { + expect(result[ix].url).to.equal(currParams.expect.pixels[ix]); + expect(result[ix].type).to.equal(currParams.expect.type); + } + }); + } + }); +}); From 06045585979e00430ef142eb58fa1ee0582fa1a4 Mon Sep 17 00:00:00 2001 From: relaido <63339139+relaido@users.noreply.github.com> Date: Mon, 13 Apr 2020 13:32:05 +0900 Subject: [PATCH 87/90] Add Relaido Adapter (#5101) * add relaido adapter * remove event listener * fixed UserSyncs and e.data Co-authored-by: ishigami_shingo --- modules/relaidoBidAdapter.js | 303 ++++++++++++++++++ modules/relaidoBidAdapter.md | 48 +++ test/spec/modules/relaidoBidAdapter_spec.js | 324 ++++++++++++++++++++ 3 files changed, 675 insertions(+) create mode 100644 modules/relaidoBidAdapter.js create mode 100644 modules/relaidoBidAdapter.md create mode 100644 test/spec/modules/relaidoBidAdapter_spec.js diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js new file mode 100644 index 00000000000..22551877a93 --- /dev/null +++ b/modules/relaidoBidAdapter.js @@ -0,0 +1,303 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const BIDDER_CODE = 'relaido'; +const BIDDER_DOMAIN = 'api.relaido.jp'; +const ADAPTER_VERSION = '1.0.0'; +const DEFAULT_TTL = 300; +const UUID_KEY = 'relaido_uuid'; + +const storage = getStorageManager(); + +function isBidRequestValid(bid) { + if (!utils.isSafariBrowser() && !hasUuid()) { + utils.logWarn('uuid is not found.'); + return false; + } + if (!utils.deepAccess(bid, 'params.placementId')) { + utils.logWarn('placementId param is reqeuired.'); + return false; + } + if (hasVideoMediaType(bid)) { + if (!isVideoValid(bid)) { + utils.logWarn('Invalid mediaType video.'); + return false; + } + } else if (hasBannerMediaType(bid)) { + if (!isBannerValid(bid)) { + utils.logWarn('Invalid mediaType banner.'); + return false; + } + } else { + utils.logWarn('Invalid mediaTypes input banner or video.'); + return false; + } + return true; +} + +function buildRequests(validBidRequests, bidderRequest) { + let bidRequests = []; + + for (let i = 0; i < validBidRequests.length; i++) { + const bidRequest = validBidRequests[i]; + const placementId = utils.getBidIdParameter('placementId', bidRequest.params); + const bidDomain = bidRequest.params.domain || BIDDER_DOMAIN; + const bidUrl = `https://${bidDomain}/vast/v1/out/bid/${placementId}`; + const uuid = getUuid(); + const mediaType = getMediaType(bidRequest); + + let payload = { + version: ADAPTER_VERSION, + ref: bidderRequest.refererInfo.referer, + timeout_ms: bidderRequest.timeout, + ad_unit_code: bidRequest.adUnitCode, + auction_id: bidRequest.auctionId, + bidder: bidRequest.bidder, + bidder_request_id: bidRequest.bidderRequestId, + bid_requests_count: bidRequest.bidRequestsCount, + bid_id: bidRequest.bidId, + transaction_id: bidRequest.transactionId, + media_type: mediaType, + uuid: uuid, + }; + + if (hasVideoMediaType(bidRequest)) { + const playerSize = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + payload.width = playerSize[0][0]; + payload.height = playerSize[0][1]; + } else if (hasBannerMediaType(bidRequest)) { + const sizes = utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes'); + payload.width = sizes[0][0]; + payload.height = sizes[0][1]; + } + + bidRequests.push({ + method: 'GET', + url: bidUrl, + data: payload, + options: { + withCredentials: true + }, + bidId: bidRequest.bidId, + player: bidRequest.params.player, + width: payload.width, + height: payload.height, + mediaType: mediaType, + }); + } + + return bidRequests; +} + +function interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; + const body = serverResponse.body; + if (!body || body.status != 'ok') { + return []; + } + + if (body.uuid) { + storage.setDataInLocalStorage(UUID_KEY, body.uuid); + } + + const playerUrl = bidRequest.player || body.playerUrl; + const mediaType = bidRequest.mediaType || VIDEO; + + let bidResponse = { + requestId: bidRequest.bidId, + width: bidRequest.width, + height: bidRequest.height, + cpm: body.price, + currency: body.currency, + creativeId: body.creativeId, + dealId: body.dealId || '', + ttl: body.ttl || DEFAULT_TTL, + netRevenue: true, + mediaType: mediaType, + }; + if (mediaType === VIDEO) { + bidResponse.vastXml = body.vast; + bidResponse.renderer = newRenderer(bidRequest.bidId, playerUrl); + } else { + const playerTag = createPlayerTag(playerUrl); + const renderTag = createRenderTag(bidRequest.width, bidRequest.height, body.vast); + bidResponse.ad = `
${playerTag}${renderTag}
`; + } + bidResponses.push(bidResponse); + + return bidResponses; +} + +function getUserSyncs(syncOptions, serverResponses) { + if (!syncOptions.iframeEnabled) { + return []; + } + let syncUrl = `https://${BIDDER_DOMAIN}/tr/v1/prebid/sync.html`; + if (serverResponses.length > 0) { + syncUrl = utils.deepAccess(serverResponses, '0.body.syncUrl') || syncUrl; + } + receiveMessage(); + return [{ + type: 'iframe', + url: syncUrl + }]; +} + +function onBidWon(bid) { + let query = utils.parseQueryStringParameters({ + placement_id: utils.deepAccess(bid, 'params.0.placementId'), + creative_id: utils.deepAccess(bid, 'creativeId'), + price: utils.deepAccess(bid, 'cpm'), + auction_id: utils.deepAccess(bid, 'auctionId'), + bid_id: utils.deepAccess(bid, 'requestId'), + ad_id: utils.deepAccess(bid, 'adId'), + ad_unit_code: utils.deepAccess(bid, 'adUnitCode'), + ref: window.location.href, + }).replace(/\&$/, ''); + const bidDomain = utils.deepAccess(bid, 'params.0.domain') || BIDDER_DOMAIN; + const burl = `https://${bidDomain}/tr/v1/prebid/win.gif?${query}`; + utils.triggerPixel(burl); +} + +function onTimeout(data) { + let query = utils.parseQueryStringParameters({ + placement_id: utils.deepAccess(data, '0.params.0.placementId'), + timeout: utils.deepAccess(data, '0.timeout'), + auction_id: utils.deepAccess(data, '0.auctionId'), + bid_id: utils.deepAccess(data, '0.bidId'), + ad_unit_code: utils.deepAccess(data, '0.adUnitCode'), + ref: window.location.href, + }).replace(/\&$/, ''); + const bidDomain = utils.deepAccess(data, '0.params.0.domain') || BIDDER_DOMAIN; + const timeoutUrl = `https://${bidDomain}/tr/v1/prebid/timeout.gif?${query}`; + utils.triggerPixel(timeoutUrl); +} + +function createPlayerTag(playerUrl) { + return ``; +} + +function createRenderTag(width, height, vast) { + return ``; +}; + +function newRenderer(bidId, playerUrl) { + const renderer = Renderer.install({ + id: bidId, + url: playerUrl, + loaded: false + }); + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('renderer.setRender Error', err); + } + return renderer; +} + +function outstreamRender(bid) { + bid.renderer.push(() => { + window.RelaidoPlayer.renderAd({ + adUnitCode: bid.adUnitCode, + width: bid.width, + height: bid.height, + vastXml: bid.vastXml, + mediaType: bid.mediaType, + }); + }); +} + +function receiveMessage() { + window.addEventListener('message', setUuid); +} + +function setUuid(e) { + if (utils.isPlainObject(e.data) && e.data.relaido_uuid) { + storage.setDataInLocalStorage(UUID_KEY, e.data.relaido_uuid); + window.removeEventListener('message', setUuid); + } +} + +function isBannerValid(bid) { + if (!isMobile()) { + return false; + } + const sizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes'); + if (sizes && utils.isArray(sizes)) { + if (utils.isArray(sizes[0])) { + const width = sizes[0][0]; + const height = sizes[0][1]; + if (width >= 300 && height >= 250) { + return true; + } + } + } + return false; +} + +function isVideoValid(bid) { + const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); + if (playerSize && utils.isArray(playerSize) && playerSize.length > 0) { + const context = utils.deepAccess(bid, 'mediaTypes.video.context'); + if (context && context === 'outstream') { + return true; + } + } + return false; +} + +function hasUuid() { + return !!storage.getDataFromLocalStorage(UUID_KEY); +} + +function getUuid() { + return storage.getDataFromLocalStorage(UUID_KEY) || ''; +} + +export function isMobile() { + const ua = navigator.userAgent; + if (ua.indexOf('iPhone') > -1 || ua.indexOf('iPod') > -1 || (ua.indexOf('Android') > -1 && ua.indexOf('Tablet') == -1)) { + return true; + } + return false; +} + +function getMediaType(bid) { + if (hasVideoMediaType(bid)) { + return VIDEO; + } else if (hasBannerMediaType(bid)) { + return BANNER; + } + return ''; +} + +function hasBannerMediaType(bid) { + return !!utils.deepAccess(bid, 'mediaTypes.banner'); +} + +function hasVideoMediaType(bid) { + return !!utils.deepAccess(bid, 'mediaTypes.video'); +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs: getUserSyncs, + onBidWon, + onTimeout +} + +registerBidder(spec); diff --git a/modules/relaidoBidAdapter.md b/modules/relaidoBidAdapter.md new file mode 100644 index 00000000000..459f772c66b --- /dev/null +++ b/modules/relaidoBidAdapter.md @@ -0,0 +1,48 @@ +# Overview + +``` +Module Name: Relaido Bidder Adapter +Module Type: Bidder Adapter +Maintainer: video-dev@cg.relaido.co.jp +``` + +# Description + +Connects to Relaido exchange for bids. + +Relaido bid adapter supports Outstream Video. + +# Test Parameters + +```javascript + var adUnits=[{ + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bids: [{ + bidder: 'relaido', + params: { + placementId: '9900' + } + }] + },{ + code: 'video-ad-player', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 360] + } + }, + bids: [{ + bidder: 'relaido', + params: { + placementId: '9900' + } + }] + }]; +``` \ No newline at end of file diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js new file mode 100644 index 00000000000..8e72548cc1f --- /dev/null +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -0,0 +1,324 @@ +import { expect } from 'chai'; +import { spec } from 'modules/relaidoBidAdapter.js'; +import * as url from 'src/url.js'; +import * as utils from 'src/utils.js'; + +const UUID_KEY = 'relaido_uuid'; +const DEFAULT_USER_AGENT = window.navigator.userAgent; +const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1'; + +const setUADefault = () => { window.navigator.__defineGetter__('userAgent', function () { return DEFAULT_USER_AGENT }) }; +const setUAMobile = () => { window.navigator.__defineGetter__('userAgent', function () { return MOBILE_USER_AGENT }) }; + +describe('RelaidoAdapter', function () { + const relaido_uuid = 'hogehoge'; + let bidRequest; + let bidderRequest; + let serverResponse; + let serverRequest; + + beforeEach(function () { + bidRequest = { + bidder: 'relaido', + params: { + placementId: '100000', + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [ + [640, 360] + ] + } + }, + adUnitCode: 'test', + bidId: '2ed93003f7bb99', + bidderRequestId: '1c50443387a1f2', + auctionId: '413ed000-8c7a-4ba1-a1fa-9732e006f8c3', + transactionId: '5c2d064c-7b76-42e8-a383-983603afdc45', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0 + }; + bidderRequest = { + timeout: 1000, + refererInfo: { + referer: 'https://publisher.com/home' + } + }; + serverResponse = { + body: { + status: 'ok', + price: 500, + model: 'vcpm', + currency: 'JPY', + creativeId: 1000, + uuid: relaido_uuid, + vast: '', + playerUrl: 'https://relaido/player.js', + syncUrl: 'https://relaido/sync.html' + } + }; + serverRequest = { + method: 'GET', + bidId: bidRequest.bidId, + width: bidRequest.mediaTypes.video.playerSize[0][0], + height: bidRequest.mediaTypes.video.playerSize[0][1], + mediaType: 'video', + }; + localStorage.setItem(UUID_KEY, relaido_uuid); + }); + + describe('spec.isBidRequestValid', function () { + it('should return true when the required params are passed by video', function () { + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true when the required params are passed by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [300, 250] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + setUADefault(); + }); + + it('should return false when the uuid are missing', function () { + localStorage.removeItem(UUID_KEY); + const result = !!(utils.isSafariBrowser()); + expect(spec.isBidRequestValid(bidRequest)).to.equal(result); + }); + + it('should return false when the placementId params are missing', function () { + bidRequest.params.placementId = undefined; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when the mediaType video params are missing', function () { + bidRequest.mediaTypes = { + video: {} + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when the mediaType banner params are missing', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: {} + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + setUADefault(); + }); + + it('should return false when the non-mobile', function () { + bidRequest.mediaTypes = { + banner: { + sizes: [ + [300, 250] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when the mediaTypes params are missing', function () { + bidRequest.mediaTypes = {}; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + + describe('spec.buildRequests', function () { + it('should build bid requests by video', function () { + const bidRequests = spec.buildRequests([bidRequest], bidderRequest); + expect(bidRequests).to.have.lengthOf(1); + const request = bidRequests[0]; + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://api.relaido.jp/vast/v1/out/bid/100000'); + expect(request.bidId).to.equal(bidRequest.bidId); + expect(request.width).to.equal(bidRequest.mediaTypes.video.playerSize[0][0]); + expect(request.height).to.equal(bidRequest.mediaTypes.video.playerSize[0][1]); + expect(request.mediaType).to.equal('video'); + expect(request.data.ref).to.equal(bidderRequest.refererInfo.referer); + expect(request.data.timeout_ms).to.equal(bidderRequest.timeout); + expect(request.data.ad_unit_code).to.equal(bidRequest.adUnitCode); + expect(request.data.auction_id).to.equal(bidRequest.auctionId); + expect(request.data.bidder).to.equal(bidRequest.bidder); + expect(request.data.bidder_request_id).to.equal(bidRequest.bidderRequestId); + expect(request.data.bid_requests_count).to.equal(bidRequest.bidRequestsCount); + expect(request.data.bid_id).to.equal(bidRequest.bidId); + expect(request.data.transaction_id).to.equal(bidRequest.transactionId); + expect(request.data.media_type).to.equal('video'); + expect(request.data.uuid).to.equal(relaido_uuid); + expect(request.data.width).to.equal(bidRequest.mediaTypes.video.playerSize[0][0]); + expect(request.data.height).to.equal(bidRequest.mediaTypes.video.playerSize[0][1]); + }); + + it('should build bid requests by banner', function () { + bidRequest.mediaTypes = { + banner: { + sizes: [ + [640, 360] + ] + } + }; + const bidRequests = spec.buildRequests([bidRequest], bidderRequest); + expect(bidRequests).to.have.lengthOf(1); + const request = bidRequests[0]; + expect(request.mediaType).to.equal('banner'); + }); + }); + + describe('spec.interpretResponse', function () { + it('should build bid response by video', function () { + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(1); + const response = bidResponses[0]; + expect(response.requestId).to.equal(serverRequest.bidId); + expect(response.width).to.equal(serverRequest.width); + expect(response.height).to.equal(serverRequest.height); + expect(response.cpm).to.equal(serverResponse.body.price); + expect(response.currency).to.equal(serverResponse.body.currency); + expect(response.creativeId).to.equal(serverResponse.body.creativeId); + expect(response.vastXml).to.equal(serverResponse.body.vast); + expect(response.ad).to.be.undefined; + }); + + it('should build bid response by banner', function () { + serverRequest.mediaType = 'banner'; + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(1); + const response = bidResponses[0]; + expect(response.requestId).to.equal(serverRequest.bidId); + expect(response.width).to.equal(serverRequest.width); + expect(response.height).to.equal(serverRequest.height); + expect(response.cpm).to.equal(serverResponse.body.price); + expect(response.currency).to.equal(serverResponse.body.currency); + expect(response.creativeId).to.equal(serverResponse.body.creativeId); + expect(response.vastXml).to.be.undefined; + expect(response.ad).to.include(`
`); + expect(response.ad).to.include(``); + expect(response.ad).to.include(`window.RelaidoPlayer.renderAd`); + }); + + it('should not build bid response', function () { + serverResponse = {}; + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(0); + }); + + it('should not build bid response', function () { + serverResponse = { + body: { + status: 'no_ad', + } + }; + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(0); + }); + }); + + describe('spec.getUserSyncs', function () { + it('should choose iframe sync urls', function () { + let userSyncs = spec.getUserSyncs({iframeEnabled: true}, [serverResponse]); + expect(userSyncs).to.deep.equal([{ + type: 'iframe', + url: serverResponse.body.syncUrl + }]); + }); + + it('should choose iframe sync urls if serverResponse are empty', function () { + let userSyncs = spec.getUserSyncs({iframeEnabled: true}, []); + expect(userSyncs).to.deep.equal([{ + type: 'iframe', + url: 'https://api.relaido.jp/tr/v1/prebid/sync.html' + }]); + }); + + it('should choose iframe sync urls if syncUrl are undefined', function () { + serverResponse.body.syncUrl = undefined; + let userSyncs = spec.getUserSyncs({iframeEnabled: true}, [serverResponse]); + expect(userSyncs).to.deep.equal([{ + type: 'iframe', + url: 'https://api.relaido.jp/tr/v1/prebid/sync.html' + }]); + }); + + it('should return empty if iframeEnabled are false', function () { + let userSyncs = spec.getUserSyncs({iframeEnabled: false}, [serverResponse]); + expect(userSyncs).to.have.lengthOf(0); + }); + }); + + describe('spec.onBidWon', function () { + let stub; + beforeEach(() => { + stub = sinon.stub(utils, 'triggerPixel'); + }); + afterEach(() => { + stub.restore(); + }); + + it('Should create nurl pixel if bid nurl', function () { + let bid = { + bidder: bidRequest.bidder, + creativeId: serverResponse.body.creativeId, + cpm: serverResponse.body.price, + params: [bidRequest.params], + auctionId: bidRequest.auctionId, + requestId: bidRequest.bidId, + adId: '3b286a4db7031f', + adUnitCode: bidRequest.adUnitCode, + ref: window.location.href, + } + spec.onBidWon(bid); + const parser = url.parse(stub.getCall(0).args[0]); + const query = parser.search; + expect(parser.hostname).to.equal('api.relaido.jp'); + expect(parser.pathname).to.equal('/tr/v1/prebid/win.gif'); + expect(query.placement_id).to.equal('100000'); + expect(query.creative_id).to.equal('1000'); + expect(query.price).to.equal('500'); + expect(query.auction_id).to.equal('413ed000-8c7a-4ba1-a1fa-9732e006f8c3'); + expect(query.bid_id).to.equal('2ed93003f7bb99'); + expect(query.ad_id).to.equal('3b286a4db7031f'); + expect(query.ad_unit_code).to.equal('test'); + expect(query.ref).to.include(window.location.href); + }); + }); + + describe('spec.onTimeout', function () { + let stub; + beforeEach(() => { + stub = sinon.stub(utils, 'triggerPixel'); + }); + afterEach(() => { + stub.restore(); + }); + + it('Should create nurl pixel if bid nurl', function () { + const data = [{ + bidder: bidRequest.bidder, + bidId: bidRequest.bidId, + adUnitCode: bidRequest.adUnitCode, + auctionId: bidRequest.auctionId, + params: [bidRequest.params], + timeout: bidderRequest.timeout, + }]; + spec.onTimeout(data); + const parser = url.parse(stub.getCall(0).args[0]); + const query = parser.search; + expect(parser.hostname).to.equal('api.relaido.jp'); + expect(parser.pathname).to.equal('/tr/v1/prebid/timeout.gif'); + expect(query.placement_id).to.equal('100000'); + expect(query.timeout).to.equal('1000'); + expect(query.auction_id).to.equal('413ed000-8c7a-4ba1-a1fa-9732e006f8c3'); + expect(query.bid_id).to.equal('2ed93003f7bb99'); + expect(query.ad_unit_code).to.equal('test'); + expect(query.ref).to.include(window.location.href); + }); + }); +}); From 5eac19632da09ed2f4852a0b497a24140ba6a773 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen <14229985+Fawke@users.noreply.github.com> Date: Mon, 13 Apr 2020 17:43:11 +0530 Subject: [PATCH 88/90] fix circle ci failing tests (#5113) --- test/spec/modules/relaidoBidAdapter_spec.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 8e72548cc1f..f0d3a2fb6d8 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -1,6 +1,5 @@ import { expect } from 'chai'; import { spec } from 'modules/relaidoBidAdapter.js'; -import * as url from 'src/url.js'; import * as utils from 'src/utils.js'; const UUID_KEY = 'relaido_uuid'; @@ -275,7 +274,7 @@ describe('RelaidoAdapter', function () { ref: window.location.href, } spec.onBidWon(bid); - const parser = url.parse(stub.getCall(0).args[0]); + const parser = utils.parseUrl(stub.getCall(0).args[0]); const query = parser.search; expect(parser.hostname).to.equal('api.relaido.jp'); expect(parser.pathname).to.equal('/tr/v1/prebid/win.gif'); @@ -309,7 +308,7 @@ describe('RelaidoAdapter', function () { timeout: bidderRequest.timeout, }]; spec.onTimeout(data); - const parser = url.parse(stub.getCall(0).args[0]); + const parser = utils.parseUrl(stub.getCall(0).args[0]); const query = parser.search; expect(parser.hostname).to.equal('api.relaido.jp'); expect(parser.pathname).to.equal('/tr/v1/prebid/timeout.gif'); From 7438f5a407cbca36ae994a14d39d5f291cbdbba8 Mon Sep 17 00:00:00 2001 From: harpere Date: Mon, 13 Apr 2020 08:21:09 -0400 Subject: [PATCH 89/90] minor consentManagement fix (#5050) Co-authored-by: Eric Harper --- modules/consentManagement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 39ebea01500..4a1fa9ba8d0 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -99,7 +99,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { if (success) { if (tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { cmpSuccess(tcfData, hookConfig); - } else if (tcfData.eventStatus === 'cmpuishown' && tcfData.tcString.length > 0 && tcfData.purposeOneTreatment === true) { + } else if (tcfData.eventStatus === 'cmpuishown' && tcfData.tcString && tcfData.purposeOneTreatment === true) { cmpSuccess(tcfData, hookConfig); } } else { From 758abf31b472309a59a87afd9459e49905efc1ad Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 13 Apr 2020 08:27:26 -0700 Subject: [PATCH 90/90] PubMatic adapter to support image sync (#5104) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * little revamp in iframe user sync code * added support for image based sync as well --- modules/pubmaticBidAdapter.js | 12 +++-- test/spec/modules/pubmaticBidAdapter_spec.js | 49 +++++++++++++++----- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 2c1024b6fe3..e9ae90bdc50 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -6,7 +6,8 @@ import {config} from '../src/config.js'; const BIDDER_CODE = 'pubmatic'; const LOG_WARN_PREFIX = 'PubMatic: '; const ENDPOINT = 'https://hbopenbid.pubmatic.com/translator?source=prebid-client'; -const USYNCURL = 'https://ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p='; +const USER_SYNC_URL_IFRAME = 'https://ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p='; +const USER_SYNC_URL_IMAGE = 'https://image8.pubmatic.com/AdServer/ImgSync?p='; const DEFAULT_CURRENCY = 'USD'; const AUCTION_TYPE = 1; const PUBMATIC_DIGITRUST_KEY = 'nFIn8aLzbd'; @@ -1057,7 +1058,7 @@ export const spec = { * Register User Sync. */ getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { - let syncurl = USYNCURL + publisherId; + let syncurl = '' + publisherId; // Attaching GDPR Consent Params in UserSync url if (gdprConsent) { @@ -1078,10 +1079,13 @@ export const spec = { if (syncOptions.iframeEnabled) { return [{ type: 'iframe', - url: syncurl + url: USER_SYNC_URL_IFRAME + syncurl }]; } else { - utils.logWarn(LOG_WARN_PREFIX + 'Please enable iframe based user sync.'); + return [{ + type: 'image', + url: USER_SYNC_URL_IMAGE + syncurl + }]; } }, diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 533edb33858..d840d2527bd 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -2711,37 +2711,53 @@ describe('PubMatic adapter', function () { }); describe('getUserSyncs', function() { - const syncurl = 'https://ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p=5670'; + const syncurl_iframe = 'https://ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p=5670'; + const syncurl_image = 'https://image8.pubmatic.com/AdServer/ImgSync?p=5670'; let sandbox; beforeEach(function () { sandbox = sinon.sandbox.create(); }); afterEach(function() { sandbox.restore(); - }) + }); - it('execute only if iframeEnabled', function() { + it('execute as per config', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ - type: 'iframe', url: syncurl + type: 'iframe', url: syncurl_iframe + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ + type: 'image', url: syncurl_image }]); - expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.equal(undefined); }); it('CCPA/USP', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&us_privacy=1NYN` + type: 'iframe', url: `${syncurl_iframe}&us_privacy=1NYN` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, '1NYN')).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&us_privacy=1NYN` }]); }); it('GDPR', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&gdpr=1&gdpr_consent=foo` + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo` }]); expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&gdpr=0&gdpr_consent=foo` + type: 'iframe', url: `${syncurl_iframe}&gdpr=0&gdpr_consent=foo` }]); expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&gdpr=1&gdpr_consent=` + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=` + }]); + + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&gdpr=1&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&gdpr=0&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&gdpr=1&gdpr_consent=` }]); }); @@ -2753,7 +2769,10 @@ describe('PubMatic adapter', function () { return config[key]; }); expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&coppa=1` + type: 'iframe', url: `${syncurl_iframe}&coppa=1` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&coppa=1` }]); }); @@ -2765,7 +2784,10 @@ describe('PubMatic adapter', function () { return config[key]; }); expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl}` + type: 'iframe', url: `${syncurl_iframe}` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ + type: 'image', url: `${syncurl_image}` }]); }); @@ -2777,7 +2799,10 @@ describe('PubMatic adapter', function () { return config[key]; }); expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&gdpr=1&gdpr_consent=foo&us_privacy=1NYN&coppa=1` + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo&us_privacy=1NYN&coppa=1` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&gdpr=1&gdpr_consent=foo&us_privacy=1NYN&coppa=1` }]); }); });