From 94d34d1783fd6712613247d6696b8cc2ca9579a6 Mon Sep 17 00:00:00 2001 From: m-oranskaya <99481039+m-oranskaya@users.noreply.github.com> Date: Thu, 18 Aug 2022 20:13:17 +1100 Subject: [PATCH 01/25] Adriver Bid and Id Modules: buyerid bug fix (#8768) * initial commit * adriver id submodule add * add id system tests, fix adriver bid adapter tests * adriver: fix buyerid * remarks fixing * removal of excess * delete custom parameter * bug fixes --- modules/adriverBidAdapter.js | 17 +++++------------ modules/adriverIdSystem.js | 3 ++- test/spec/modules/adriverBidAdapter_spec.js | 19 ++++++------------- test/spec/modules/adriverIdSystem_spec.js | 11 +++-------- 4 files changed, 16 insertions(+), 34 deletions(-) diff --git a/modules/adriverBidAdapter.js b/modules/adriverBidAdapter.js index e95f83d2c7b..b19c8318754 100644 --- a/modules/adriverBidAdapter.js +++ b/modules/adriverBidAdapter.js @@ -1,6 +1,6 @@ // ADRIVER BID ADAPTER for Prebid 1.13 import { logInfo, getWindowLocation, getBidIdParameter, _each } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'adriver'; @@ -22,8 +22,6 @@ export const spec = { }, buildRequests: function (validBidRequests, bidderRequest) { - logInfo('validBidRequests', validBidRequests); - let win = getWindowLocation(); let customID = Math.round(Math.random() * 999999999) + '-' + Math.round(new Date() / 1000) + '-1-46-'; let siteId = getBidIdParameter('siteid', validBidRequests[0].params) + ''; @@ -99,22 +97,17 @@ export const spec = { }); }); - let userid = validBidRequests[0].userId; - let adrcidCookie = storage.getDataFromLocalStorage('adrcid') || validBidRequests[0].userId.adrcid; - + let adrcidCookie = storage.getDataFromLocalStorage('adrcid') || validBidRequests[0].userId?.adrcid; if (adrcidCookie) { - payload.adrcid = adrcidCookie; - payload.id5 = userid.id5id; - payload.sharedid = userid.pubcid; - payload.unifiedid = userid.tdid; + payload.user.buyerid = adrcidCookie; } const payloadString = JSON.stringify(payload); return { method: 'POST', url: ADRIVER_BID_URL, - data: payloadString, - }; + data: payloadString + } }, interpretResponse: function (serverResponse, bidRequest) { diff --git a/modules/adriverIdSystem.js b/modules/adriverIdSystem.js index 6a492fac508..fb8ce99ec16 100644 --- a/modules/adriverIdSystem.js +++ b/modules/adriverIdSystem.js @@ -73,7 +73,8 @@ export const adriverIdSubmodule = { callback(); } }; - ajax(url, callbacks, undefined, {method: 'GET'}); + let newUrl = url + '&cid=' + (storage.getDataFromLocalStorage('adrcid') || storage.getCookie('adrcid')); + ajax(newUrl, callbacks, undefined, {method: 'GET'}); } }; return {callback: resp}; diff --git a/test/spec/modules/adriverBidAdapter_spec.js b/test/spec/modules/adriverBidAdapter_spec.js index 12c0a15fb06..33084877c14 100644 --- a/test/spec/modules/adriverBidAdapter_spec.js +++ b/test/spec/modules/adriverBidAdapter_spec.js @@ -297,25 +297,18 @@ describe('adriverAdapter', function () { { adrcid: undefined } ] cookieValues.forEach(cookieValue => describe('test cookie exist or not behavior', function () { - let expectedValues = { - adrcid: cookieValue.adrcid, - at: '', - cur: '', - tmax: '', - site: '', - id: '', - user: '', - device: '', - imp: '' - } + let expectedValues = [ + 'buyerid', + 'ext' + ] it('check adrcid if it exists', function () { bidRequests[0].userId.adrcid = cookieValue.adrcid; const payload = JSON.parse(spec.buildRequests(bidRequests).data); if (cookieValue.adrcid) { - expect(Object.keys(payload)).to.have.members(Object.keys(expectedValues)); + expect(Object.keys(payload.user)).to.have.members(expectedValues); } else { - expect(payload.adrcid).to.equal(undefined); + expect(payload.user.buyerid).to.equal(0); } }); })); diff --git a/test/spec/modules/adriverIdSystem_spec.js b/test/spec/modules/adriverIdSystem_spec.js index 29d965d5ed4..bc7d3f191d2 100644 --- a/test/spec/modules/adriverIdSystem_spec.js +++ b/test/spec/modules/adriverIdSystem_spec.js @@ -32,7 +32,6 @@ describe('AdriverIdSystem', function () { expect(request.url).to.include('https://ad.adriver.ru/cgi-bin/json.cgi'); request.respond(503, null, 'Unavailable'); expect(logErrorStub.calledOnce).to.be.true; - expect(callbackSpy.calledOnce).to.be.true; }); it('test call user sync url with the right params', function() { @@ -67,16 +66,12 @@ describe('AdriverIdSystem', function () { let request = server.requests[0]; request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ adrcid: response.adrcid })); - let expectedExpiration = new Date(); - expectedExpiration.setTime(expectedExpiration.getTime() + 86400 * 1825 * 1000); + let now = new Date(); + now.setTime(now.getTime() + 86400 * 1825 * 1000); const minimalDate = new Date(0).toString(); - function dateStringFor(date, maxDeltaMs = 2000) { - return sinon.match((val) => Math.abs(date.getTime() - new Date(val).getTime()) <= maxDeltaMs) - } - if (response.adrcid) { - expect(setCookieStub.calledWith('adrcid', response.adrcid, dateStringFor(expectedExpiration))).to.be.true; + expect(setCookieStub.calledWith('adrcid', response.adrcid, now.toUTCString())).to.be.true; expect(setLocalStorageStub.calledWith('adrcid', response.adrcid)).to.be.true; } else { expect(setCookieStub.calledWith('adrcid', '', minimalDate)).to.be.false; From 940339a3ebb9d425af261ab1535d32b3717c49ac Mon Sep 17 00:00:00 2001 From: Love Sharma Date: Thu, 18 Aug 2022 09:36:21 -0400 Subject: [PATCH 02/25] handle native response privacy link (#8838) Co-authored-by: Zicong Zhou --- src/native.js | 1 + test/spec/native_spec.js | 144 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) diff --git a/src/native.js b/src/native.js index 470ac0b9a32..0e48470d300 100644 --- a/src/native.js +++ b/src/native.js @@ -735,6 +735,7 @@ function toLegacyResponse(ortbResponse, ortbRequest) { const legacyResponse = {}; const requestAssets = ortbRequest?.assets || []; legacyResponse.clickUrl = ortbResponse.link.url; + legacyResponse.privacyLink = ortbResponse.privacy; for (const asset of ortbResponse?.assets || []) { const requestAsset = requestAssets.find(reqAsset => asset.id === reqAsset.id); if (asset.title) { diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index 19f417bd88f..08491edeb64 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -44,6 +44,106 @@ const bid = { }, }; +const ortbBid = { + adId: '123', + transactionId: 'au', + native: { + ortb: { + assets: [ + { + id: 0, + title: { + text: 'Native Creative' + } + }, + { + id: 1, + data: { + value: 'Cool description great stuff' + } + }, + { + id: 2, + data: { + value: 'Do it' + } + }, + { + id: 3, + img: { + url: 'http://cdn.example.com/p/creative-image/image.png', + h: 83, + w: 127 + } + }, + { + id: 4, + img: { + url: 'http://cdn.example.com/p/creative-image/icon.jpg', + h: 742, + w: 989 + } + }, + { + id: 5, + data: { + value: 'AppNexus', + type: 1 + } + } + ], + link: { + url: 'https://www.link.example' + }, + privacy: 'https://privacy-link.example', + ver: '1.2' + } + }, +}; + +const ortbRequest = { + assets: [ + { + id: 0, + required: 0, + title: { + len: 140 + } + }, { + id: 1, + required: 0, + data: { + type: 2 + } + }, { + id: 2, + required: 0, + data: { + type: 12 + } + }, { + id: 3, + required: 0, + img: { + type: 3 + } + }, { + id: 4, + required: 0, + img: { + type: 1 + } + }, { + id: 5, + required: 0, + data: { + type: 1 + } + } + ], + ver: '1.2' +} + const bidWithUndefinedFields = { transactionId: 'au', native: { @@ -401,6 +501,50 @@ describe('native.js', function () { }); }); + it('creates native all asset message with OpenRTB format', function () { + const messageRequest = { + message: 'Prebid Native', + action: 'allAssetRequest', + adId: '123', + }; + + const message = getAllAssetsMessage(messageRequest, ortbBid, {getNativeReq: () => ortbRequest}); + + expect(message.assets.length).to.equal(8); + expect(message.assets).to.deep.include({ + key: 'body', + value: bid.native.body, + }); + expect(message.assets).to.deep.include({ + key: 'image', + value: bid.native.image.url, + }); + expect(message.assets).to.deep.include({ + key: 'clickUrl', + value: bid.native.clickUrl, + }); + expect(message.assets).to.deep.include({ + key: 'title', + value: bid.native.title, + }); + expect(message.assets).to.deep.include({ + key: 'icon', + value: bid.native.icon.url, + }); + expect(message.assets).to.deep.include({ + key: 'cta', + value: bid.native.cta, + }); + expect(message.assets).to.deep.include({ + key: 'sponsoredBy', + value: bid.native.sponsoredBy, + }); + expect(message.assets).to.deep.include({ + key: 'privacyLink', + value: ortbBid.native.ortb.privacy, + }); + }); + const SAMPLE_ORTB_REQUEST = toOrtbNativeRequest({ title: 'vtitle', body: 'vbody' From 8ef12d1f21b3866f2b47c2a65c22ed49b767496d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9onard=20Labat?= Date: Thu, 18 Aug 2022 15:49:58 +0200 Subject: [PATCH 03/25] Criteo Bid Adapter - Add support for banner+native multiformat ad unit (#8842) Previously, the use of a native adunit was exclusive with the banner type. --- modules/criteoBidAdapter.js | 32 +++++++----- test/spec/modules/criteoBidAdapter_spec.js | 61 +++++++++++++--------- 2 files changed, 54 insertions(+), 39 deletions(-) diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 1c269b2bba4..98aa58f5a70 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -469,15 +469,20 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (bidRequest.params.publisherSubId) { slot.publishersubid = bidRequest.params.publisherSubId; } - if (bidRequest.params.nativeCallback || deepAccess(bidRequest, `mediaTypes.${NATIVE}`)) { + + if (bidRequest.params.nativeCallback || hasNativeMediaType(bidRequest)) { slot.native = true; if (!checkNativeSendId(bidRequest)) { logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); } - slot.sizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes'), parseNativeSize); - } else { + } + + if (hasBannerMediaType(bidRequest)) { slot.sizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes'), parseSize); + } else { + slot.sizes = []; } + if (hasVideoMediaType(bidRequest)) { const video = { playersizes: parseSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize'), parseSize), @@ -554,17 +559,18 @@ function parseSize(size) { return size[0] + 'x' + size[1]; } -function parseNativeSize(size) { - if (size[0] === undefined && size[1] === undefined) { - return '2x2'; - } - return size[0] + 'x' + size[1]; -} - function hasVideoMediaType(bidRequest) { return deepAccess(bidRequest, 'mediaTypes.video') !== undefined; } +function hasBannerMediaType(bidRequest) { + return deepAccess(bidRequest, 'mediaTypes.banner') !== undefined; +} + +function hasNativeMediaType(bidRequest) { + return deepAccess(bidRequest, 'mediaTypes.native') !== undefined; +} + function hasValidVideoMediaType(bidRequest) { let isValid = true; @@ -646,18 +652,18 @@ function enrichSlotWithFloors(slot, bidRequest) { if (bidRequest.mediaTypes?.banner) { slotFloors.banner = {}; const bannerSizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes')) - bannerSizes.forEach(bannerSize => slotFloors.banner[parseSize(bannerSize).toString()] = bidRequest.getFloor({size: bannerSize, mediaType: BANNER})); + bannerSizes.forEach(bannerSize => slotFloors.banner[parseSize(bannerSize).toString()] = bidRequest.getFloor({ size: bannerSize, mediaType: BANNER })); } if (bidRequest.mediaTypes?.video) { slotFloors.video = {}; const videoSizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize')) - videoSizes.forEach(videoSize => slotFloors.video[parseSize(videoSize).toString()] = bidRequest.getFloor({size: videoSize, mediaType: VIDEO})); + videoSizes.forEach(videoSize => slotFloors.video[parseSize(videoSize).toString()] = bidRequest.getFloor({ size: videoSize, mediaType: VIDEO })); } if (bidRequest.mediaTypes?.native) { slotFloors.native = {}; - slotFloors.native['*'] = bidRequest.getFloor({size: '*', mediaType: NATIVE}); + slotFloors.native['*'] = bidRequest.getFloor({ size: '*', mediaType: NATIVE }); } if (Object.keys(slotFloors).length > 0) { diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index 7252232676f..7af5f5d77a2 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -13,7 +13,7 @@ import * as utils from 'src/utils.js'; import * as refererDetection from 'src/refererDetection.js'; import { config } from '../../../src/config.js'; import * as storageManager from 'src/storageManager.js'; -import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; +import { BANNER, NATIVE, VIDEO } from '../../../src/mediaTypes.js'; describe('The Criteo bidding adapter', function () { let utilsMock, sandbox; @@ -707,7 +707,7 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.slots[0].sizes[0]).to.equal('undefinedxundefined'); }); - it('should properly detect and get sizes of native sizeless banner', function () { + it('should properly detect and forward native flag', function () { const bidRequests = [ { mediaTypes: { @@ -722,11 +722,10 @@ describe('The Criteo bidding adapter', function () { ]; const request = spec.buildRequests(bidRequests, bidderRequest); const ortbRequest = request.data; - expect(ortbRequest.slots[0].sizes).to.have.lengthOf(1); - expect(ortbRequest.slots[0].sizes[0]).to.equal('2x2'); + expect(ortbRequest.slots[0].native).to.equal(true); }); - it('should properly detect and get size of native sizeless banner', function () { + it('should properly detect and forward native flag', function () { const bidRequests = [ { mediaTypes: { @@ -741,8 +740,7 @@ describe('The Criteo bidding adapter', function () { ]; const request = spec.buildRequests(bidRequests, bidderRequest); const ortbRequest = request.data; - expect(ortbRequest.slots[0].sizes).to.have.lengthOf(1); - expect(ortbRequest.slots[0].sizes[0]).to.equal('2x2'); + expect(ortbRequest.slots[0].native).to.equal(true); }); it('should properly build a networkId request', function () { @@ -1258,11 +1256,13 @@ describe('The Criteo bidding adapter', function () { if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { return { currency: 'USD', - floor: 1.0}; + floor: 1.0 + }; } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { return { currency: 'USD', - floor: 2.0}; + floor: 2.0 + }; } else { return {} } @@ -1273,9 +1273,10 @@ describe('The Criteo bidding adapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.slots[0].ext.floors).to.deep.equal({ 'banner': { - '300x250': {'currency': 'USD', 'floor': 1}, - '728x90': {'currency': 'USD', 'floor': 2} - }}); + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + }); }); it('should properly build a video request with several player sizes with floors', function () { @@ -1297,11 +1298,13 @@ describe('The Criteo bidding adapter', function () { if (inputParams.mediaType === VIDEO && inputParams.size[0] === 300 && inputParams.size[1] === 250) { return { currency: 'USD', - floor: 1.0}; + floor: 1.0 + }; } else if (inputParams.mediaType === VIDEO && inputParams.size[0] === 728 && inputParams.size[1] === 90) { return { currency: 'USD', - floor: 2.0}; + floor: 2.0 + }; } else { return {} } @@ -1312,9 +1315,10 @@ describe('The Criteo bidding adapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.slots[0].ext.floors).to.deep.equal({ 'video': { - '300x250': {'currency': 'USD', 'floor': 1}, - '728x90': {'currency': 'USD', 'floor': 2} - }}); + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + }); }); it('should properly build a multi format request with floors', function () { @@ -1347,19 +1351,23 @@ describe('The Criteo bidding adapter', function () { if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { return { currency: 'USD', - floor: 1.0}; + floor: 1.0 + }; } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { return { currency: 'USD', - floor: 2.0}; + floor: 2.0 + }; } else if (inputParams.mediaType === VIDEO && inputParams.size[0] === 640 && inputParams.size[1] === 480) { return { currency: 'EUR', - floor: 3.2}; + floor: 3.2 + }; } else if (inputParams.mediaType === NATIVE && inputParams.size === '*') { return { currency: 'YEN', - floor: 4.99}; + floor: 4.99 + }; } else { return {} } @@ -1371,15 +1379,16 @@ describe('The Criteo bidding adapter', function () { expect(request.data.slots[0].ext.data.someContextAttribute).to.deep.equal('abc'); expect(request.data.slots[0].ext.floors).to.deep.equal({ 'banner': { - '300x250': {'currency': 'USD', 'floor': 1}, - '728x90': {'currency': 'USD', 'floor': 2} + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } }, 'video': { - '640x480': {'currency': 'EUR', 'floor': 3.2} + '640x480': { 'currency': 'EUR', 'floor': 3.2 } }, 'native': { - '*': {'currency': 'YEN', 'floor': 4.99} - }}); + '*': { 'currency': 'YEN', 'floor': 4.99 } + } + }); }); }); From 442931dd2f257826db611725120e0ee748423d74 Mon Sep 17 00:00:00 2001 From: wsusrasp <106743463+wsusrasp@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:51:46 +0200 Subject: [PATCH 04/25] Ras Bid Adapter: support for SlotSequence parameter (#8792) * add rasbidadapter pos param * Read pos off the adunit * rename conflicting pos parameter for clarity --- modules/rasBidAdapter.js | 8 ++++++++ modules/rasBidAdapter.md | 27 +++++++++++++------------ test/spec/modules/rasBidAdapter_spec.js | 2 ++ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/modules/rasBidAdapter.js b/modules/rasBidAdapter.js index 909b6a7b795..7bc3cf66b0d 100644 --- a/modules/rasBidAdapter.js +++ b/modules/rasBidAdapter.js @@ -1,3 +1,4 @@ +import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { isEmpty, getAdUnitSizes, parseSizesInput, deepAccess } from '../src/utils.js'; @@ -92,11 +93,18 @@ const getSlots = (bidRequests) => { const batchSize = bidRequests.length; for (let i = 0; i < batchSize; i++) { const adunit = bidRequests[i]; + const slotSequence = utils.deepAccess(adunit, 'params.slotSequence'); + const sizes = parseSizesInput(getAdUnitSizes(adunit)).join(','); + queryString += `&slot${i}=${encodeURIComponent(adunit.params.slot)}&id${i}=${encodeURIComponent(adunit.bidId)}&composition${i}=CHILD`; + if (sizes.length) { queryString += `&iusizes${i}=${encodeURIComponent(sizes)}`; } + if (slotSequence !== undefined) { + queryString += `&pos${i}=${encodeURIComponent(slotSequence)}`; + } } return queryString; }; diff --git a/modules/rasBidAdapter.md b/modules/rasBidAdapter.md index 5cf75c3446d..384ba0b611f 100644 --- a/modules/rasBidAdapter.md +++ b/modules/rasBidAdapter.md @@ -34,17 +34,18 @@ var adUnits = [{ # Parameters -| Name | Scope | Type | Description | Example -| --- | --- | --- | --- | --- -| network | required | String | Specific identifier provided by RAS | `"4178463"` -| site | required | String | Specific identifier name (case-insensitive) that is associated with this ad unit and provided by RAS | `"example_com"` -| area | required | String | Ad unit category name; only case-insensitive alphanumeric with underscores and hyphens are allowed | `"sport"` -| slot | required | String | Ad unit placement name (case-insensitive) provided by RAS | `"slot"` -| pageContext | optional | Object | Web page context data | `{}` -| pageContext.dr | optional | String | Document referrer URL address | `"https://example.com/"` -| pageContext.du | optional | String | Document URL address | `"https://example.com/sport/football/article.html?id=932016a5-02fc-4d5c-b643-fafc2f270f06"` +| Name | Scope | Type | Description | Example +| --- | --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- +| network | required | String | Specific identifier provided by RAS | `"4178463"` +| site | required | String | Specific identifier name (case-insensitive) that is associated with this ad unit and provided by RAS | `"example_com"` +| area | required | String | Ad unit category name; only case-insensitive alphanumeric with underscores and hyphens are allowed | `"sport"` +| slot | required | String | Ad unit placement name (case-insensitive) provided by RAS | `"slot"` +| slotSequence | optional | Number | Ad unit sequence position provided by RAS | `1` +| pageContext | optional | Object | Web page context data | `{}` +| pageContext.dr | optional | String | Document referrer URL address | `"https://example.com/"` +| pageContext.du | optional | String | Document URL address | `"https://example.com/sport/football/article.html?id=932016a5-02fc-4d5c-b643-fafc2f270f06"` | pageContext.dv | optional | String | Document virtual address as slash-separated path that may consist of any number of parts (case-insensitive alphanumeric with underscores and hyphens); first part should be the same as `site` value and second as `area` value; next parts may reflect website navigation | `"example_com/sport/football"` -| pageContext.keyWords | optional | String[] | List of keywords associated with this ad unit; only case-insensitive alphanumeric with underscores and hyphens are allowed | `["euro", "lewandowski"]` -| pageContext.keyValues | optional | Object | Key-values associated with this ad unit (case-insensitive); following characters are not allowed in the values: `" ' = ! + # * ~ ; ^ ( ) < > [ ] & @` | `{}` -| pageContext.keyValues.ci | optional | String | Content unique identifier | `"932016a5-02fc-4d5c-b643-fafc2f270f06"` -| pageContext.keyValues.adunit | optional | String | Ad unit name | `"example_com/sport"` +| pageContext.keyWords | optional | String[] | List of keywords associated with this ad unit; only case-insensitive alphanumeric with underscores and hyphens are allowed | `["euro", "lewandowski"]` +| pageContext.keyValues | optional | Object | Key-values associated with this ad unit (case-insensitive); following characters are not allowed in the values: `" ' = ! + # * ~ ; ^ ( ) < > [ ] & @` | `{}` +| pageContext.keyValues.ci | optional | String | Content unique identifier | `"932016a5-02fc-4d5c-b643-fafc2f270f06"` +| pageContext.keyValues.adunit | optional | String | Ad unit name | `"example_com/sport"` diff --git a/test/spec/modules/rasBidAdapter_spec.js b/test/spec/modules/rasBidAdapter_spec.js index 324bf782672..8c378aaa416 100644 --- a/test/spec/modules/rasBidAdapter_spec.js +++ b/test/spec/modules/rasBidAdapter_spec.js @@ -58,6 +58,7 @@ describe('rasBidAdapter', function () { slot: 'test', area: 'areatest', site: 'test', + slotSequence: '0', network: '4178463' } }; @@ -140,6 +141,7 @@ describe('rasBidAdapter', function () { expect(requests[0].url).to.have.string('DV=test%2Fareatest'); expect(requests[0].url).to.have.string('kwrd=val1%2Bval2'); expect(requests[0].url).to.have.string('kvadunit=test%2Fareatest'); + expect(requests[0].url).to.have.string('pos0=0'); }); }); From d77309aad8fc00faaddf42da163ace477c41e91e Mon Sep 17 00:00:00 2001 From: Catalin Ciocov Date: Thu, 18 Aug 2022 17:54:53 +0300 Subject: [PATCH 05/25] Improve Digital adapter: refactor code to align with latest RAZR creative tags (#8827) --- modules/improvedigitalBidAdapter.js | 78 ++++++++++++------- src/adloader.js | 3 +- .../modules/improvedigitalBidAdapter_spec.js | 22 +++--- 3 files changed, 61 insertions(+), 42 deletions(-) diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 4996f0efaf0..c8fc8eb7a2a 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -19,6 +19,7 @@ import {Renderer} from '../src/Renderer.js'; import {createEidsArray} from './userId/eids.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {loadExternalScript} from '../src/adloader.js'; const BIDDER_CODE = 'improvedigital'; const CREATIVE_TTL = 300; @@ -212,7 +213,7 @@ export const spec = { ID_RESPONSE.buildAd(bid, bidRequest, bidObject); - ID_RAZR.addBidData({ + ID_RAZR.forwardBid({ bidRequest, bid }); @@ -640,37 +641,58 @@ const ID_OUTSTREAM = { }; const ID_RAZR = { - RENDERER_URL: 'https://razr.improvedigital.com/renderer.js', - addBidData({bid, bidRequest}) { - if (this.isValidBid(bid)) { - bid.renderer = Renderer.install({ - url: this.RENDERER_URL, - config: {bidRequest} - }); - bid.renderer.setRender(this.render); + RENDERER_URL: 'https://cdn.360yield.com/razr/tag.js', + + forwardBid({bidRequest, bid}) { + if (bid.mediaType !== BANNER) { + return; } - }, - isValidBid(bid) { - return bid && /razr:\/\//.test(bid.ad); + const cfg = { + prebid: { + bidRequest, + bid + } + }; + + const cfgStr = JSON.stringify(cfg).replace(/<\/script>/g, '\\x3C/script>'); + const s = ``; + bid.ad = bid.ad.replace(/]*>/, match => match + s); + + this.installListener(); }, - render(bid) { - const {bidRequest} = bid.renderer.getConfig(); - - const payload = { - type: 'prebid', - bidRequest, - bid, - config: mergeDeep( - {}, - config.getConfig('improvedigital.rendererConfig'), - deepAccess(bidRequest, 'params.rendererConfig') - ) - }; + installListener() { + if (this._listenerInstalled) { + return; + } + + window.addEventListener('message', function(e) { + const data = e.data?.razr?.load; + if (!data) { + return; + } + + if (e.source) { + data.source = e.source; + if (data.id) { + e.source.postMessage({ + razr: { + id: data.id + } + }, '*'); + } + } + + const ns = window.razr = window.razr || {}; + ns.q = ns.q || []; + ns.q.push(data); + + if (!ns.loaded) { + loadExternalScript(ID_RAZR.RENDERER_URL, BIDDER_CODE); + } + }); - const razr = window.razr = window.razr || {}; - razr.queue = razr.queue || []; - razr.queue.push(payload); + this._listenerInstalled = true; } }; diff --git a/src/adloader.js b/src/adloader.js index 1e7995a9dc6..6b7427d3e52 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -18,7 +18,8 @@ const _approvedLoadExternalJSList = [ 'ftrackId', 'inskin', 'hadron', - 'medianet' + 'medianet', + 'improvedigital' ] /** diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index a882f5ca5ef..19e91cbde96 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -1031,7 +1031,7 @@ describe('Improve Digital Adapter Tests', function () { width: 728, height: 90, ttl: 300, - ad: '  ', + ad: '  ', creativeId: '510265', dealId: 320896, netRevenue: false, @@ -1042,6 +1042,12 @@ describe('Improve Digital Adapter Tests', function () { } ]; + const multiFormatExpectedBid = [ + Object.assign({}, expectedBid[0], { + ad: '  ' + }) + ]; + const expectedTwoBids = [ expectedBid[0], { @@ -1051,7 +1057,7 @@ describe('Improve Digital Adapter Tests', function () { width: 300, height: 250, ttl: 300, - ad: '  ', + ad: '  ', creativeId: '479163', dealId: 320896, netRevenue: false, @@ -1091,7 +1097,7 @@ describe('Improve Digital Adapter Tests', function () { it('should return a well-formed display bid for multi-format ad unit', function () { const bids = spec.interpretResponse(serverResponse, {bidderRequest: multiFormatBidderRequest}); - expect(bids).to.deep.equal(expectedBid); + expect(bids).to.deep.equal(multiFormatExpectedBid); }); it('should return two bids', function () { @@ -1233,16 +1239,6 @@ describe('Improve Digital Adapter Tests', function () { bids = spec.interpretResponse(videoResponse, {bidderRequest: multiFormatBidderRequest}); expect(bids[0].mediaType).to.equal(VIDEO); }); - - it('should not affect non-RAZR bids', function () { - const bids = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(bids[0].renderer).to.not.exist; - }); - - it('should detect RAZR bids', function () { - const bids = spec.interpretResponse(serverResponseRazr, {bidderRequest}); - expect(bids[0].renderer).to.exist; - }); }); describe('getUserSyncs', function () { From 4e9ccd5b207f7195bdd420b7ed0af6a29b16fb0d Mon Sep 17 00:00:00 2001 From: Love Sharma Date: Thu, 18 Aug 2022 10:57:33 -0400 Subject: [PATCH 06/25] IX Bid Adapter: Native OpenRTB Request Support (#8853) * fix native click trackers to only fire on click * fix unit tests for ix * remove version for native requests * remove unnecessary request conversion Co-authored-by: Zicong Zhou --- modules/ixBidAdapter.js | 319 +------------------------ test/spec/modules/ixBidAdapter_spec.js | 314 ++++++++++++++++-------- 2 files changed, 223 insertions(+), 410 deletions(-) diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index ce4fdcc2431..61e518d306d 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -25,7 +25,6 @@ import {find} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import {Renderer} from '../src/Renderer.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const BIDDER_CODE = 'ix'; const ALIAS_BIDDER_CODE = 'roundel'; @@ -100,93 +99,6 @@ const VIDEO_PARAMS_ALLOW_LIST = [ 'delivery', 'pos', 'companionad', 'api', 'companiontype', 'ext', 'playerSize', 'w', 'h' ]; -const NATIVE_ASSET_TYPES = { - TITLE: 100, - IMG: 200, - VIDEO: 300, - DATA: 400 -}; -const NATIVE_IMAGE_TYPES = { - ICON: 1, - MAIN: 3 -}; -const NATIVE_DATA_TYPES = { - SPONSORED: 1, - DESC: 2, - RATING: 3, - LIKES: 4, - DOWNLOADS: 5, - PRICE: 6, - SALEPRICE: 7, - PHONE: 8, - ADDRESS: 9, - DESC2: 10, - DISPLAYURL: 11, - CTATEXT: 12 -}; -const NATIVE_DATA_MAP = { - [NATIVE_DATA_TYPES.SPONSORED]: 'sponsoredBy', - [NATIVE_DATA_TYPES.DESC]: 'body', - [NATIVE_DATA_TYPES.RATING]: 'rating', - [NATIVE_DATA_TYPES.LIKES]: 'likes', - [NATIVE_DATA_TYPES.DOWNLOADS]: 'downloads', - [NATIVE_DATA_TYPES.PRICE]: 'price', - [NATIVE_DATA_TYPES.SALEPRICE]: 'salePrice', - [NATIVE_DATA_TYPES.PHONE]: 'phone', - [NATIVE_DATA_TYPES.ADDRESS]: 'address', - [NATIVE_DATA_TYPES.DESC2]: 'body2', - [NATIVE_DATA_TYPES.DISPLAYURL]: 'displayUrl', - [NATIVE_DATA_TYPES.CTATEXT]: 'cta' -}; -const NATIVE_ASSETS_MAP = { - 'title': { assetType: NATIVE_ASSET_TYPES.TITLE }, - 'icon': { assetType: NATIVE_ASSET_TYPES.IMG, subtype: NATIVE_IMAGE_TYPES.ICON }, - 'image': { assetType: NATIVE_ASSET_TYPES.IMG, subtype: NATIVE_IMAGE_TYPES.MAIN }, - 'sponsoredBy': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.SPONSORED }, - 'body': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DESC }, - 'rating': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.RATING }, - 'likes': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.LIKES }, - 'downloads': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DOWNLOADS }, - 'price': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.PRICE }, - 'salePrice': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.SALEPRICE }, - 'phone': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.PHONE }, - 'address': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.ADDRESS }, - 'body2': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DESC2 }, - 'displayUrl': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DISPLAYURL }, - 'cta': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.CTATEXT }, - 'video': { assetType: NATIVE_ASSET_TYPES.VIDEO } -}; -const NATIVE_ALLOWED_PROPERTIES = [ - 'rendererUrl', - 'sendTargetingKeys', - 'adTemplate', - 'type', - 'ext', - 'privacyLink', - 'clickUrl', - 'privacyIcon' -]; -const NATIVE_ASSET_DEFAULT = { - TITLE: { - LEN: 25 - }, - VIDEO: { - MIMES: [ - 'video/mp4', - 'video/webm' - ], - MINDURATION: 0, - MAXDURATION: 120, - PROTOCOLS: [2, 3, 5, 6], - } -}; -const NATIVE_EVENT_TYPES = { - IMRESSION: 1 -}; -const NATIVE_EVENT_TRACKING_METHOD = { - IMG: 1, - JS: 2 -}; const LOCAL_STORAGE_KEY = 'ixdiag'; let hasRegisteredHandler = false; export const storage = getStorageManager({gvlid: GLOBAL_VENDOR_ID, bidderCode: BIDDER_CODE}); @@ -307,50 +219,13 @@ function bidToVideoImp(bid) { */ function bidToNativeImp(bid) { const imp = bidToImp(bid); - const nativeAdUnitRef = deepAccess(bid, 'mediaTypes.native'); - - const assets = []; - - // Convert all native assets to imp object - for (const [adUnitProperty, adUnitValues] of Object.entries(nativeAdUnitRef)) { - if (!NATIVE_ASSETS_MAP[adUnitProperty]) { - continue; - } - - const { assetType, subtype } = NATIVE_ASSETS_MAP[adUnitProperty]; - let asset; - switch (assetType) { - case NATIVE_ASSET_TYPES.TITLE: - asset = createNativeTitleRequest(adUnitValues); - break; - case NATIVE_ASSET_TYPES.IMG: - asset = createNativeImgRequest(adUnitValues, subtype); - break; - case NATIVE_ASSET_TYPES.VIDEO: - asset = createNativeVideoRequest(adUnitValues); - break; - case NATIVE_ASSET_TYPES.DATA: - asset = createNativeDataRequest(adUnitValues, subtype); - break; - } - asset.id = assetType + (subtype || 0); - assets.push(asset); - } - - if (assets.length === 0) { - logWarn('IX Bid Adapter: Native bid does not contain recognised assets in [mediaTypes.native]'); - return {}; - } - const request = { - assets: assets, - ver: '1.2', - eventtrackers: [{ - event: 1, - methods: [1, 2] - }], - privacy: 1 - }; + const request = bid.nativeOrtbRequest + request.eventtrackers = [{ + event: 1, + methods: [1, 2] + }]; + request.privacy = 1; imp.native = { request: JSON.stringify(request), @@ -365,80 +240,6 @@ function bidToNativeImp(bid) { return imp; } -/** - * Converts native bid asset to a native impression asset - * @param {object} bidAsset PBJS bid asset object - * @returns {object} IX impression asset object - */ -function createNativeTitleRequest(bidAsset) { - return { - required: bidAsset.required ? 1 : 0, - title: { - len: bidAsset.len ? bidAsset.len : NATIVE_ASSET_DEFAULT.TITLE.LEN, - ext: bidAsset.ext - } - } -} - -/** - * Converts native bid asset to a native impression asset - * @param {object} bidAsset PBJS bid asset object - * @param {int} type The image type - * @returns {object} IX impression asset object - */ -function createNativeImgRequest(bidAsset, type) { - let asset = { - required: bidAsset.required ? 1 : 0, - img: { - type: type, - mimes: bidAsset.mimes, - ext: bidAsset.ext - } - } - - if (bidAsset.hasOwnProperty('sizes') && bidAsset.sizes.length === 2) { - asset.img.wmin = bidAsset.sizes[0]; - asset.img.hmin = bidAsset.sizes[1]; - } - - return asset -} - -/** - * Converts native bid asset to a native impression asset - * @param {object} bidAsset PBJS bid asset object - * @returns {object} IX impression asset object - */ -function createNativeVideoRequest(bidAsset) { - return { - required: bidAsset.required ? 1 : 0, - video: { - mimes: bidAsset.mimes ? bidAsset.mimes : NATIVE_ASSET_DEFAULT.VIDEO.MIMES, - minduration: bidAsset.minduration ? bidAsset.minduration : NATIVE_ASSET_DEFAULT.VIDEO.MINDURATION, - maxduration: bidAsset.maxduration ? bidAsset.maxduration : NATIVE_ASSET_DEFAULT.VIDEO.MAXDURATION, - protocols: bidAsset.protocols ? bidAsset.protocols : NATIVE_ASSET_DEFAULT.VIDEO.PROTOCOLS, - ext: bidAsset.ext - } - } -} - -/** - * Converts native bid asset to a native impression asset - * @param {object} bidAsset PBJS bid asset object - * @param {int} type The image type - * @returns {object} IX impression asset object - */ -function createNativeDataRequest(bidAsset, type) { - return { - required: bidAsset.required ? 1 : 0, - data: { - type: type, - len: bidAsset.len, - ext: bidAsset.ext - } - } -} - /** * Converts an incoming PBJS bid to an IX Impression * @param {object} bid PBJS bid object @@ -559,7 +360,7 @@ function parseBid(rawBid, currency, bidRequest) { bid.mediaTypes = bidRequest.mediaTypes; bid.ttl = isValidExpiry ? rawBid.exp : VIDEO_TIME_TO_LIVE; } else if (parsedAdm && parsedAdm.native) { - bid.native = interpretNativeAdm(parsedAdm.native); + bid.native = {ortb: parsedAdm.native}; bid.width = rawBid.w ? rawBid.w : 1; bid.height = rawBid.h ? rawBid.h : 1; bid.mediaType = NATIVE; @@ -583,84 +384,6 @@ function parseBid(rawBid, currency, bidRequest) { return bid; } -/** - * Parse native adm and set native asset key names recognized by Prebid.js - * @param {string} adm Native adm complience - */ -function interpretNativeAdm(nativeResponse) { - const native = { - clickUrl: nativeResponse.link.url, - privacyLink: nativeResponse.privacy - }; - - for (const asset of nativeResponse.assets) { - const subtype = asset.id % 100; - const assetType = asset.id - subtype; - - switch (assetType) { - case NATIVE_ASSET_TYPES.TITLE: - native.title = asset.title && asset.title.text; - break; - case NATIVE_ASSET_TYPES.IMG: - const image = { - url: asset.img && asset.img.url, - height: asset.img && asset.img.h, - width: asset.img && asset.img.w - }; - native[subtype === NATIVE_IMAGE_TYPES.ICON ? 'icon' : 'image'] = image; - break; - case NATIVE_ASSET_TYPES.VIDEO: - native.video = asset.video && asset.video.vasttag; - break; - case NATIVE_ASSET_TYPES.DATA: - setDataAsset(native, asset, subtype); - break; - default: - logWarn(`IX Bid Adapter: native asset ID ${asset.id} could not be recognized`); - } - } - - setTrackers(native, nativeResponse); - return native; -} - -function setDataAsset(native, asset, type) { - if (!(type in NATIVE_DATA_MAP)) { - logWarn(`IX Bid Adapter: native data asset type ${type} is not supported`); - return; - } - native[NATIVE_DATA_MAP[type]] = asset.data && asset.data.value; -} - -function setTrackers(native, nativeResponse) { - native.impressionTrackers = [] - - if (Array.isArray(nativeResponse.imptrackers)) { - native.impressionTrackers.push(...nativeResponse.imptrackers) - } - - if (Array.isArray(nativeResponse.link.clicktrackers)) { - native.impressionTrackers.push(...nativeResponse.link.clicktrackers) - } - - if (Array.isArray(nativeResponse.eventtrackers)) { - nativeResponse.eventtrackers.forEach(tracker => { - if (tracker.event !== NATIVE_EVENT_TYPES.IMRESSION) { - return - } - - switch (tracker.method) { - case NATIVE_EVENT_TRACKING_METHOD.IMG: - native.impressionTrackers.push(tracker.url); - break; - case NATIVE_EVENT_TRACKING_METHOD.JS: - native.javascriptTrackers = ``; - break; - } - }) - } -} - /** * Determines whether or not the given object is valid size format. * @@ -758,25 +481,13 @@ function isValidBidFloorParams(bidFloor, bidFloorCur) { bidFloorCur.match(curRegex)); } -function nativeMediaTypeValid(nativeObj) { - if (nativeObj === undefined) { - return true; - } - - let hasValidAsset = false; - - for (const property in nativeObj) { - if (!(property in NATIVE_ASSETS_MAP) && !NATIVE_ALLOWED_PROPERTIES.includes(property)) { - logError('IX Bid Adapter: native', { bidder: BIDDER_CODE, code: ERROR_CODES.PROPERTY_NOT_INCLUDED }); - return false; - } - - if (property in NATIVE_ASSETS_MAP) { - hasValidAsset = true; - } +function nativeMediaTypeValid(bid) { + const nativeMediaTypes = deepAccess(bid, 'mediaTypes.native'); + if (nativeMediaTypes === undefined) { + return true } - return hasValidAsset; + return bid.nativeOrtbRequest && Array.isArray(bid.nativeOrtbRequest.assets) && bid.nativeOrtbRequest.assets.length > 0 } /** @@ -837,8 +548,6 @@ function getEidInfo(allEids) { * */ function buildRequest(validBidRequests, bidderRequest, impressions, version) { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); // Always use secure HTTPS protocol. let baseUrl = SECURE_BID_URL; // Get ids from Prebid User ID Modules @@ -966,7 +675,6 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { // Use the siteId in the first bid request as the main siteId. siteID = validBidRequests[0].params.siteId; payload.s = siteID; - payload.v = version; if (version) { payload.v = version; } @@ -1603,7 +1311,6 @@ export const spec = { const paramsSize = deepAccess(bid, 'params.size'); const mediaTypeBannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); const mediaTypeVideoRef = deepAccess(bid, 'mediaTypes.video'); - const mediaTypeNativeRef = deepAccess(bid, 'mediaTypes.native'); const mediaTypeVideoPlayerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); const hasBidFloor = bid.params.hasOwnProperty('bidFloor'); const hasBidFloorCur = bid.params.hasOwnProperty('bidFloorCur'); @@ -1670,7 +1377,7 @@ export const spec = { } } - return nativeMediaTypeValid(mediaTypeNativeRef); + return nativeMediaTypeValid(bid); }, /** diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 243b702f03d..7360024eed2 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -10,7 +10,6 @@ describe('IndexexchangeAdapter', function () { const IX_SECURE_ENDPOINT = 'https://htlb.casalemedia.com/openrtb/pbjs'; const VIDEO_ENDPOINT_VERSION = 8.1; const BANNER_ENDPOINT_VERSION = 7.2; - const NATIVE_ENDPOINT_VERSION = undefined; const SAMPLE_SCHAIN = { 'ver': '1.0', @@ -378,6 +377,7 @@ describe('IndexexchangeAdapter', function () { required: false }, title: { + len: 25, required: true }, body: { @@ -387,13 +387,20 @@ describe('IndexexchangeAdapter', function () { required: true }, video: { - required: false + required: false, + mimes: ['video/mp4', 'video/webm'], + minduration: 0, + maxduration: 120, + protocols: [2, 3, 5, 6] }, sponsoredBy: { required: true } } }, + nativeOrtbRequest: { + assets: [{id: 0, required: 0, img: {type: 1}}, {id: 1, required: 1, title: {len: 140}}, {id: 2, required: 1, data: {type: 2}}, {id: 3, required: 1, img: {type: 3}}, {id: 4, required: false, video: {mimes: ['video/mp4', 'video/webm'], minduration: 0, maxduration: 120, protocols: [2, 3, 5, 6]}}] + }, adUnitCode: 'div-gpt-ad-1460505748563-0', transactionId: '173f49a8-7549-4218-a23c-e7ba59b47231', bidId: '1a2b3c4f', @@ -432,6 +439,9 @@ describe('IndexexchangeAdapter', function () { } } }, + nativeOrtbRequest: { + assets: [{id: 0, required: 0, img: {type: 1}}, {id: 1, required: 1, title: {len: 140}}, {id: 2, required: 1, data: {type: 2}}, {id: 3, required: 1, img: {type: 3}}, {id: 4, required: false, video: {mimes: ['video/mp4', 'video/webm'], minduration: 0, maxduration: 120, protocols: [2, 3, 5, 6]}}] + }, adUnitCode: 'div-gpt-ad-1460505748562-0', transactionId: '173f49a8-7549-4218-a23c-e7ba59b47230', bidId: '1a2b3c4e', @@ -442,7 +452,7 @@ describe('IndexexchangeAdapter', function () { ]; const DEFAULT_NATIVE_IMP = { - request: '{"assets":[{"required":0,"img":{"type":1},"id":201},{"required":1,"title":{"len":25},"id":100},{"required":1,"data":{"type":2},"id":402},{"required":1,"img":{"type":3},"id":203},{"required":0,"video":{"mimes":["video/mp4","video/webm"],"minduration":0,"maxduration":120,"protocols":[2,3,5,6]},"id":300},{"required":1,"data":{"type":1},"id":401}],"ver":"1.2","eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', + request: '{"assets":[{"id":0,"required":0,"img":{"type":1}},{"id":1,"required":1,"title":{"len":140}},{"id":2,"required":1,"data":{"type":2}},{"id":3,"required":1,"img":{"type":3}},{"id":4,"required":false,"video":{"mimes":["video/mp4","video/webm"],"minduration":0,"maxduration":120,"protocols":[2,3,5,6]}}],"eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', ver: '1.2' } @@ -588,7 +598,7 @@ describe('IndexexchangeAdapter', function () { advbrandid: 303325, advbrand: 'OECTA' }, - adm: '{"native":{"ver":"1.2","assets":[{"id":201,"img":{"url":"https://cdn.liftoff.io/customers/1209/creatives/2501-icon-250x250.png","w":250,"h":250}},{"id":203,"img":{"url":"https://cdn.liftoff.io/customers/5a9cab9cc6/image/lambda_png/a0355879b06c09b09232.png","w":1200,"h":627}},{"id":401,"data":{"value":"autodoc.co.uk"}},{"id":402,"data":{"value":"Les pièces automobiles dont vous avez besoin, toujours sous la main."}},{"id":100,"title":{"text":"Autodoc"}},{"id":300,"video":{"vasttag":"blah"}}],"link":{"url":"https://play.google.com/store/apps/details?id=de.autodoc.gmbh","clicktrackers":["https://click.liftoff.io/v1/campaign_click/blah"]},"eventtrackers":[{"event":1,"method":1,"url":"https://impression-europe.liftoff.io/index/impression"},{"event":1,"method":1,"url":"https://a701.casalemedia.com/impression/v1"}],"privacy":"https://privacy.link.com"}}' + adm: '{"native":{"ver":"1.2","assets":[{"id":0,"img":{"url":"https://cdn.liftoff.io/customers/1209/creatives/2501-icon-250x250.png","w":250,"h":250}},{"id":1,"img":{"url":"https://cdn.liftoff.io/customers/5a9cab9cc6/image/lambda_png/a0355879b06c09b09232.png","w":1200,"h":627}},{"id":2,"data":{"value":"autodoc.co.uk"}},{"id":3,"data":{"value":"Les pièces automobiles dont vous avez besoin, toujours sous la main."}},{"id":4,"title":{"text":"Autodoc"}},{"id":5,"video":{"vasttag":"blah"}}],"link":{"url":"https://play.google.com/store/apps/details?id=de.autodoc.gmbh","clicktrackers":["https://click.liftoff.io/v1/campaign_click/blah"]},"eventtrackers":[{"event":1,"method":1,"url":"https://impression-europe.liftoff.io/index/impression"},{"event":1,"method":1,"url":"https://a701.casalemedia.com/impression/v1"}],"privacy":"https://privacy.link.com"}}' } ], seat: '3970' @@ -864,7 +874,7 @@ describe('IndexexchangeAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should return true when required params found for a banner or video ad', function () { + it('should return true when required params found for a banner, video or native ad', function () { expect(spec.isBidRequestValid(DEFAULT_BANNER_VALID_BID[0])).to.equal(true); expect(spec.isBidRequestValid(DEFAULT_VIDEO_VALID_BID[0])).to.equal(true); expect(spec.isBidRequestValid(DEFAULT_NATIVE_VALID_BID[0])).to.equal(true); @@ -1064,15 +1074,12 @@ describe('IndexexchangeAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('should fail when native contains unrecongized properties', function () { + it('should fail if native openRTB object contains no valid assets', function () { let bid = utils.deepClone(DEFAULT_NATIVE_VALID_BID[0]); - bid.mediaTypes.native.test = {} + bid.nativeOrtbRequest = {} expect(spec.isBidRequestValid(bid)).to.be.false; - }); - it('should fail if native mediaTypes should contains no valid assets', function () { - let bid = utils.deepClone(DEFAULT_NATIVE_VALID_BID[0]); - bid.mediaTypes.native = {} + bid.nativeOrtbRequest = {assets: []} expect(spec.isBidRequestValid(bid)).to.be.false; }); }); @@ -2145,7 +2152,7 @@ describe('IndexexchangeAdapter', function () { it('should have native request', () => { const nativeImpression = JSON.parse(request[1].data.r).imp[0]; - expect(request[1].data.v).to.equal(NATIVE_ENDPOINT_VERSION); + expect(request[1].data.hasOwnProperty('v')).to.equal(false); expect(nativeImpression.id).to.equal(DEFAULT_NATIVE_VALID_BID[0].bidId); expect(nativeImpression.native).to.deep.equal(DEFAULT_NATIVE_IMP); }); @@ -2491,7 +2498,7 @@ describe('IndexexchangeAdapter', function () { const request = spec.buildRequests(DEFAULT_NATIVE_VALID_BID, DEFAULT_OPTION); const query = request[0].data; - expect(query.v).to.equal(NATIVE_ENDPOINT_VERSION); + expect(query.hasOwnProperty('v')).to.equal(false); expect(query.s).to.equal(DEFAULT_NATIVE_VALID_BID[0].params.siteId); expect(query.r).to.exist; expect(query.ac).to.equal('j'); @@ -2516,92 +2523,148 @@ describe('IndexexchangeAdapter', function () { const request = spec.buildRequests(DEFAULT_NATIVE_VALID_BID, DEFAULT_OPTION); const nativeImpression = JSON.parse(request[0].data.r).imp[0]; - expect(request[0].data.v).to.equal(NATIVE_ENDPOINT_VERSION); + expect(request[0].data.hasOwnProperty('v')).to.equal(false); expect(nativeImpression.id).to.equal(DEFAULT_NATIVE_VALID_BID[0].bidId); expect(nativeImpression.native).to.deep.equal(DEFAULT_NATIVE_IMP); }); it('should build request with given asset properties', function() { let bid = utils.deepClone(DEFAULT_NATIVE_VALID_BID) - bid[0].mediaTypes.native = { - title: { - len: 200 - }, - video: { - mimes: [ - 'javascript' - ], - minduration: 10, - maxduration: 60, - protocols: [1] - } + bid[0].nativeOrtbRequest = { + assets: [{id: 0, required: 0, title: {len: 140}}, {id: 1, required: 0, video: {mimes: ['javascript'], minduration: 10, maxduration: 60, protocols: [1]}}] } const request = spec.buildRequests(bid, DEFAULT_OPTION); const nativeImpression = JSON.parse(request[0].data.r).imp[0]; - expect(nativeImpression.native).to.deep.equal({request: '{"assets":[{"required":0,"title":{"len":200},"id":100},{"required":0,"video":{"mimes":["javascript"],"minduration":10,"maxduration":60,"protocols":[1]},"id":300}],"ver":"1.2","eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', ver: '1.2'}); + expect(nativeImpression.native).to.deep.equal({request: '{"assets":[{"id":0,"required":0,"title":{"len":140}},{"id":1,"required":0,"video":{"mimes":["javascript"],"minduration":10,"maxduration":60,"protocols":[1]}}],"eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', ver: '1.2'}); }); it('should build request with all possible Prebid asset properties', function() { let bid = utils.deepClone(DEFAULT_NATIVE_VALID_BID) - bid[0].mediaTypes.native = { - title: { - required: false - }, - body: { - required: false - }, - body2: { - required: false - }, - sponsoredBy: { - required: false - }, - icon: { - required: false - }, - image: { - required: false - }, - clickUrl: { - required: false - }, - displayUrl: { - required: false - }, - privacyLink: { - required: false - }, - privacyIcon: { - required: false - }, - cta: { - required: false - }, - rating: { - required: false - }, - downloads: { - required: false - }, - likes: { - required: false - }, - price: { - required: false - }, - salePrice: { - required: false - }, - address: { - required: false - }, - phone: { - required: false - }, + bid[0].nativeOrtbRequest = { + 'ver': '1.2', + 'assets': [ + { + 'id': 0, + 'required': 0, + 'title': { + 'len': 140 + } + }, + { + 'id': 1, + 'required': 0, + 'data': { + 'type': 2 + } + }, + { + 'id': 2, + 'required': 0, + 'data': { + 'type': 10 + } + }, + { + 'id': 3, + 'required': 0, + 'data': { + 'type': 1 + } + }, + { + 'id': 4, + 'required': 0, + 'img': { + 'type': 1 + } + }, + { + 'id': 5, + 'required': 0, + 'img': { + 'type': 3 + } + }, + { + 'id': 6, + 'required': 0 + }, + { + 'id': 7, + 'required': 0, + 'data': { + 'type': 11 + } + }, + { + 'id': 8, + 'required': 0 + }, + { + 'id': 9, + 'required': 0 + }, + { + 'id': 10, + 'required': 0, + 'data': { + 'type': 12 + } + }, + { + 'id': 11, + 'required': 0, + 'data': { + 'type': 3 + } + }, + { + 'id': 12, + 'required': 0, + 'data': { + 'type': 5 + } + }, + { + 'id': 13, + 'required': 0, + 'data': { + 'type': 4 + } + }, + { + 'id': 14, + 'required': 0, + 'data': { + 'type': 6 + } + }, + { + 'id': 15, + 'required': 0, + 'data': { + 'type': 7 + } + }, + { + 'id': 16, + 'required': 0, + 'data': { + 'type': 9 + } + }, + { + 'id': 17, + 'required': 0, + 'data': { + 'type': 8 + } + } + ] } const request = spec.buildRequests(bid, DEFAULT_OPTION); const nativeImpression = JSON.parse(request[0].data.r).imp[0]; - expect(nativeImpression.native).to.deep.equal({request: '{"assets":[{"required":0,"title":{"len":25},"id":100},{"required":0,"data":{"type":2},"id":402},{"required":0,"data":{"type":10},"id":410},{"required":0,"data":{"type":1},"id":401},{"required":0,"img":{"type":1},"id":201},{"required":0,"img":{"type":3},"id":203},{"required":0,"data":{"type":11},"id":411},{"required":0,"data":{"type":12},"id":412},{"required":0,"data":{"type":3},"id":403},{"required":0,"data":{"type":5},"id":405},{"required":0,"data":{"type":4},"id":404},{"required":0,"data":{"type":6},"id":406},{"required":0,"data":{"type":7},"id":407},{"required":0,"data":{"type":9},"id":409},{"required":0,"data":{"type":8},"id":408}],"ver":"1.2","eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', ver: '1.2'}); + expect(nativeImpression.native).to.deep.equal({request: '{"ver":"1.2","assets":[{"id":0,"required":0,"title":{"len":140}},{"id":1,"required":0,"data":{"type":2}},{"id":2,"required":0,"data":{"type":10}},{"id":3,"required":0,"data":{"type":1}},{"id":4,"required":0,"img":{"type":1}},{"id":5,"required":0,"img":{"type":3}},{"id":6,"required":0},{"id":7,"required":0,"data":{"type":11}},{"id":8,"required":0},{"id":9,"required":0},{"id":10,"required":0,"data":{"type":12}},{"id":11,"required":0,"data":{"type":3}},{"id":12,"required":0,"data":{"type":5}},{"id":13,"required":0,"data":{"type":4}},{"id":14,"required":0,"data":{"type":6}},{"id":15,"required":0,"data":{"type":7}},{"id":16,"required":0,"data":{"type":9}},{"id":17,"required":0,"data":{"type":8}}],"eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', ver: '1.2'}); }) }); @@ -3091,27 +3154,70 @@ describe('IndexexchangeAdapter', function () { advertiserDomains: ['www.abc.com'] }, native: { - body: 'Les pièces automobiles dont vous avez besoin, toujours sous la main.', - clickUrl: 'https://play.google.com/store/apps/details?id=de.autodoc.gmbh', - icon: { - height: 250, - width: 250, - url: 'https://cdn.liftoff.io/customers/1209/creatives/2501-icon-250x250.png' - }, - image: { - height: 627, - width: 1200, - url: 'https://cdn.liftoff.io/customers/5a9cab9cc6/image/lambda_png/a0355879b06c09b09232.png' - }, - impressionTrackers: [ - 'https://click.liftoff.io/v1/campaign_click/blah', - 'https://impression-europe.liftoff.io/index/impression', - 'https://a701.casalemedia.com/impression/v1' - ], - privacyLink: 'https://privacy.link.com', - sponsoredBy: 'autodoc.co.uk', - title: 'Autodoc', - video: 'blah' + ortb: { + assets: [ + { + 'id': 0, + 'img': { + 'h': 250, + 'url': 'https://cdn.liftoff.io/customers/1209/creatives/2501-icon-250x250.png', + 'w': 250 + } + }, + { + 'id': 1, + 'img': { + 'h': 627, + 'url': 'https://cdn.liftoff.io/customers/5a9cab9cc6/image/lambda_png/a0355879b06c09b09232.png', + 'w': 1200 + } + }, + { + 'data': { + 'value': 'autodoc.co.uk' + }, + 'id': 2 + }, + { + 'data': { + 'value': 'Les pièces automobiles dont vous avez besoin, toujours sous la main.' + }, + 'id': 3 + }, + { + 'id': 4, + 'title': { + 'text': 'Autodoc' + } + }, + { + 'id': 5, + 'video': { + 'vasttag': 'blah' + } + } + ], + 'eventtrackers': [ + { + 'event': 1, + 'method': 1, + 'url': 'https://impression-europe.liftoff.io/index/impression' + }, + { + 'event': 1, + 'method': 1, + 'url': 'https://a701.casalemedia.com/impression/v1' + } + ], + 'link': { + 'clicktrackers': [ + 'https://click.liftoff.io/v1/campaign_click/blah' + ], + 'url': 'https://play.google.com/store/apps/details?id=de.autodoc.gmbh' + }, + 'privacy': 'https://privacy.link.com', + 'ver': '1.2' + } }, ttl: 3600 } From 296a081949b11a09b789f17f1b7dba5a72b7d5b3 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Thu, 18 Aug 2022 13:46:13 -0400 Subject: [PATCH 07/25] Update Sonobi adapter with GVLID (#8860) --- modules/sonobiBidAdapter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index c6100ac8b40..3c841cc4d8a 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -11,6 +11,7 @@ const OUTSTREAM_REDNERER_URL = 'https://mtrx.go.sonobi.com/sbi_outstream_rendere export const spec = { code: BIDDER_CODE, + gvlid: 104, supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. From ed74e44a6cec85a6eb572d19033b57e2cef7a482 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 18 Aug 2022 11:06:44 -0700 Subject: [PATCH 08/25] dgkeyword RTD provider: fix tests causing ID5 test failures (#8862) --- modules/dgkeywordRtdProvider.js | 9 +++++++++ test/spec/modules/dgkeywordRtdProvider_spec.js | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/dgkeywordRtdProvider.js b/modules/dgkeywordRtdProvider.js index cea33014144..e2a29375f25 100644 --- a/modules/dgkeywordRtdProvider.js +++ b/modules/dgkeywordRtdProvider.js @@ -25,6 +25,15 @@ export function getDgKeywordsAndSet(reqBidsConfigObj, callback, moduleConfig, us const timeout = (moduleConfig && moduleConfig.params && moduleConfig.params.timeout && Number(moduleConfig.params.timeout) > 0) ? Number(moduleConfig.params.timeout) : PROFILE_TIMEOUT_MS; const url = (moduleConfig && moduleConfig.params && moduleConfig.params.url) ? moduleConfig.params.url : URL + encodeURIComponent(window.location.href); const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + callback = (function(cb) { + let done = false; + return function () { + if (!done) { + done = true; + return cb.apply(this, arguments); + } + } + })(callback); let isFinish = false; logMessage('[dgkeyword sub module]', adUnits, timeout); let setKeywordTargetBidders = getTargetBidderOfDgKeywords(adUnits); diff --git a/test/spec/modules/dgkeywordRtdProvider_spec.js b/test/spec/modules/dgkeywordRtdProvider_spec.js index a145f429557..5ecec48f6b4 100644 --- a/test/spec/modules/dgkeywordRtdProvider_spec.js +++ b/test/spec/modules/dgkeywordRtdProvider_spec.js @@ -293,8 +293,8 @@ describe('Digital Garage Keyword Module', function () { moduleConfig, null ); + const request = server.requests[0]; setTimeout(() => { - const request = server.requests[0]; if (request) { request.respond( 200, From f69cc666f12d03fbb5b0c4342d17f71103dd1d2f Mon Sep 17 00:00:00 2001 From: Scott Menzer Date: Thu, 18 Aug 2022 19:02:43 -0400 Subject: [PATCH 09/25] Id5 id configurable fetch flow (#8784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Paweł Kowalski --- modules/id5IdSystem.js | 271 ++++++---- modules/id5IdSystem.md | 30 +- test/spec/modules/id5IdSystem_spec.js | 701 +++++++++++++++++++------- 3 files changed, 705 insertions(+), 297 deletions(-) diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 96ec1fed754..95ce5c94e46 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -7,19 +7,19 @@ import { deepAccess, - logInfo, deepSetValue, - logError, isEmpty, isEmptyStr, + logError, + logInfo, logWarn, safeJSONParse } from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import { submodule } from '../src/hook.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { uspDataHandler } from '../src/adapterManager.js'; +import {ajax} from '../src/ajax.js'; +import {submodule} from '../src/hook.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {uspDataHandler} from '../src/adapterManager.js'; const MODULE_NAME = 'id5Id'; const GVLID = 131; @@ -28,10 +28,11 @@ export const ID5_STORAGE_NAME = 'id5id'; export const ID5_PRIVACY_STORAGE_NAME = `${ID5_STORAGE_NAME}_privacy`; const LOCAL_STORAGE = 'html5'; const LOG_PREFIX = 'User ID - ID5 submodule: '; +const ID5_API_CONFIG_URL = 'https://id5-sync.com/api/config/prebid' // order the legacy cookie names in reverse priority order so the last // cookie in the array is the most preferred to use -const LEGACY_COOKIE_NAMES = [ 'pbjs-id5id', 'id5id.1st', 'id5id' ]; +const LEGACY_COOKIE_NAMES = ['pbjs-id5id', 'id5id.1st', 'id5id']; export const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME}); @@ -102,92 +103,27 @@ export const id5IdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function getId - * @param {SubmoduleConfig} config + * @param {SubmoduleConfig} submoduleConfig * @param {ConsentData} consentData * @param {(Object|undefined)} cacheIdObj * @returns {IdResponse|undefined} */ - getId(config, consentData, cacheIdObj) { - if (!hasRequiredConfig(config)) { + getId(submoduleConfig, consentData, cacheIdObj) { + if (!hasRequiredConfig(submoduleConfig)) { return undefined; } - const url = `https://id5-sync.com/g/v2/${config.params.partner}.json`; - const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; - const usp = uspDataHandler.getConsentData(); - const referer = getRefererInfo(); - const signature = (cacheIdObj && cacheIdObj.signature) ? cacheIdObj.signature : getLegacyCookieSignature(); - const data = { - 'partner': config.params.partner, - 'gdpr': hasGdpr, - 'nbPage': incrementNb(config.params.partner), - 'o': 'pbjs', - 'rf': referer.topmostLocation, - 'top': referer.reachedTop ? 1 : 0, - 'u': referer.stack[0] || window.location.href, - 'v': '$prebid.version$' - }; - - // pass in optional data, but only if populated - if (hasGdpr && typeof consentData.consentString !== 'undefined' && !isEmpty(consentData.consentString) && !isEmptyStr(consentData.consentString)) { - data.gdpr_consent = consentData.consentString; - } - if (typeof usp !== 'undefined' && !isEmpty(usp) && !isEmptyStr(usp)) { - data.us_privacy = usp; - } - if (typeof signature !== 'undefined' && !isEmptyStr(signature)) { - data.s = signature; - } - if (typeof config.params.pd !== 'undefined' && !isEmptyStr(config.params.pd)) { - data.pd = config.params.pd; - } - if (typeof config.params.provider !== 'undefined' && !isEmptyStr(config.params.provider)) { - data.provider = config.params.provider; - } - - const abTestingConfig = getAbTestingConfig(config); - if (abTestingConfig.enabled === true) { - data.ab_testing = { - enabled: true, - control_group_pct: abTestingConfig.controlGroupPct // The server validates - }; - } - - const resp = function (callback) { - const callbacks = { - success: response => { - let responseObj; - if (response) { - try { - responseObj = JSON.parse(response); - logInfo(LOG_PREFIX + 'response received from the server', responseObj); - - resetNb(config.params.partner); - - if (responseObj.privacy) { - storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(responseObj.privacy), NB_EXP_DAYS); - } - - // TODO: remove after requiring publishers to use localstorage and - // all publishers have upgraded - if (config.storage.type === LOCAL_STORAGE) { - removeLegacyCookies(config.params.partner); - } - } catch (error) { - logError(LOG_PREFIX + error); - } - } - callback(responseObj); - }, - error: error => { + const resp = function (cbFunction) { + new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData()).execute() + .then(response => { + cbFunction(response) + }) + .catch(error => { logError(LOG_PREFIX + 'getId fetch encountered an error', error); - callback(); - } - }; - logInfo(LOG_PREFIX + 'requesting an ID from the server', data); - ajax(url, callbacks, JSON.stringify(data), { method: 'POST', withCredentials: true }); + cbFunction(); + }); }; - return { callback: resp }; + return {callback: resp}; }, /** @@ -212,6 +148,139 @@ export const id5IdSubmodule = { } }; +class IdFetchFlow { + constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData) { + this.submoduleConfig = submoduleConfig + this.gdprConsentData = gdprConsentData + this.cacheIdObj = cacheIdObj + this.usPrivacyData = usPrivacyData + } + + execute() { + return this.#callForConfig(this.submoduleConfig) + .then(fetchFlowConfig => { + return this.#callForExtensions(fetchFlowConfig.extensionsCall) + .then(extensionsData => { + return this.#callId5Fetch(fetchFlowConfig.fetchCall, extensionsData) + }) + }) + .then(fetchCallResponse => { + try { + resetNb(this.submoduleConfig.params.partner); + if (fetchCallResponse.privacy) { + storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(fetchCallResponse.privacy), NB_EXP_DAYS); + } + } catch (error) { + logError(LOG_PREFIX + error); + } + return fetchCallResponse; + }) + } + + #ajaxPromise(url, data, options) { + return new Promise((resolve, reject) => { + ajax(url, + { + success: function (res) { + resolve(res) + }, + error: function (err) { + reject(err) + } + }, data, options) + }) + } + + // eslint-disable-next-line no-dupe-class-members + #callForConfig(submoduleConfig) { + let url = submoduleConfig.params.configUrl || ID5_API_CONFIG_URL; // override for debug/test purposes only + return this.#ajaxPromise(url, JSON.stringify(submoduleConfig), {method: 'POST'}) + .then(response => { + let responseObj = JSON.parse(response); + logInfo(LOG_PREFIX + 'config response received from the server', responseObj); + return responseObj; + }); + } + + // eslint-disable-next-line no-dupe-class-members + #callForExtensions(extensionsCallConfig) { + if (extensionsCallConfig === undefined) { + return Promise.resolve(undefined) + } + let extensionsUrl = extensionsCallConfig.url + let method = extensionsCallConfig.method || 'GET' + let data = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {}) + return this.#ajaxPromise(extensionsUrl, data, {'method': method}) + .then(response => { + let responseObj = JSON.parse(response); + logInfo(LOG_PREFIX + 'extensions response received from the server', responseObj); + return responseObj; + }) + } + + // eslint-disable-next-line no-dupe-class-members + #callId5Fetch(fetchCallConfig, extensionsData) { + let url = fetchCallConfig.url; + let additionalData = fetchCallConfig.overrides || {}; + let data = { + ...this.#createFetchRequestData(), + ...additionalData, + extensions: extensionsData + }; + return this.#ajaxPromise(url, JSON.stringify(data), {method: 'POST', withCredentials: true}) + .then(response => { + let responseObj = JSON.parse(response); + logInfo(LOG_PREFIX + 'fetch response received from the server', responseObj); + return responseObj; + }); + } + + // eslint-disable-next-line no-dupe-class-members + #createFetchRequestData() { + const params = this.submoduleConfig.params; + const hasGdpr = (this.gdprConsentData && typeof this.gdprConsentData.gdprApplies === 'boolean' && this.gdprConsentData.gdprApplies) ? 1 : 0; + const referer = getRefererInfo(); + const signature = (this.cacheIdObj && this.cacheIdObj.signature) ? this.cacheIdObj.signature : getLegacyCookieSignature(); + const nbPage = incrementNb(params.partner); + const data = { + 'partner': params.partner, + 'gdpr': hasGdpr, + 'nbPage': nbPage, + 'o': 'pbjs', + 'rf': referer.topmostLocation, + 'top': referer.reachedTop ? 1 : 0, + 'u': referer.stack[0] || window.location.href, + 'v': '$prebid.version$', + 'storage': this.submoduleConfig.storage + }; + + // pass in optional data, but only if populated + if (hasGdpr && this.gdprConsentData.consentString !== undefined && !isEmpty(this.gdprConsentData.consentString) && !isEmptyStr(this.gdprConsentData.consentString)) { + data.gdpr_consent = this.gdprConsentData.consentString; + } + if (this.usPrivacyData !== undefined && !isEmpty(this.usPrivacyData) && !isEmptyStr(this.usPrivacyData)) { + data.us_privacy = this.usPrivacyData; + } + if (signature !== undefined && !isEmptyStr(signature)) { + data.s = signature; + } + if (params.pd !== undefined && !isEmptyStr(params.pd)) { + data.pd = params.pd; + } + if (params.provider !== undefined && !isEmptyStr(params.provider)) { + data.provider = params.provider; + } + const abTestingConfig = params.abTesting || {enabled: false}; + + if (abTestingConfig.enabled) { + data.ab_testing = { + enabled: true, control_group_pct: abTestingConfig.controlGroupPct // The server validates + }; + } + return data + } +} + function hasRequiredConfig(config) { if (!config || !config.params || !config.params.partner || typeof config.params.partner !== 'number') { logError(LOG_PREFIX + 'partner required to be defined as a number'); @@ -242,25 +311,29 @@ export function expDaysStr(expDays) { export function nbCacheName(partnerId) { return `${ID5_STORAGE_NAME}_${partnerId}_nb`; } + export function storeNbInCache(partnerId, nb) { storeInLocalStorage(nbCacheName(partnerId), nb, NB_EXP_DAYS); } + export function getNbFromCache(partnerId) { let cacheNb = getFromLocalStorage(nbCacheName(partnerId)); return (cacheNb) ? parseInt(cacheNb) : 0; } + function incrementNb(partnerId) { const nb = (getNbFromCache(partnerId) + 1); storeNbInCache(partnerId, nb); return nb; } + function resetNb(partnerId) { storeNbInCache(partnerId, 0); } function getLegacyCookieSignature() { let legacyStoredValue; - LEGACY_COOKIE_NAMES.forEach(function(cookie) { + LEGACY_COOKIE_NAMES.forEach(function (cookie) { if (storage.getCookie(cookie)) { legacyStoredValue = safeJSONParse(storage.getCookie(cookie)) || legacyStoredValue; } @@ -268,21 +341,6 @@ function getLegacyCookieSignature() { return (legacyStoredValue && legacyStoredValue.signature) || ''; } -/** - * Remove our legacy cookie values. Needed until we move all publishers - * to html5 storage in a future release - * @param {integer} partnerId - */ -function removeLegacyCookies(partnerId) { - logInfo(LOG_PREFIX + 'removing legacy cookies'); - LEGACY_COOKIE_NAMES.forEach(function(cookie) { - storage.setCookie(`${cookie}`, ' ', expDaysStr(-1)); - storage.setCookie(`${cookie}_nb`, ' ', expDaysStr(-1)); - storage.setCookie(`${cookie}_${partnerId}_nb`, ' ', expDaysStr(-1)); - storage.setCookie(`${cookie}_last`, ' ', expDaysStr(-1)); - }); -} - /** * This will make sure we check for expiration before accessing local storage * @param {string} key @@ -303,6 +361,7 @@ export function getFromLocalStorage(key) { storage.removeDataFromLocalStorage(key); return null; } + /** * Ensure that we always set an expiration in local storage since * by default it's not required @@ -315,14 +374,4 @@ export function storeInLocalStorage(key, value, expDays) { storage.setDataInLocalStorage(`${key}`, value); } -/** - * gets the existing abTesting config or generates a default config with abTesting off - * - * @param {SubmoduleConfig|undefined} config - * @returns {Object} an object which always contains at least the property "enabled" - */ -function getAbTestingConfig(config) { - return deepAccess(config, 'params.abTesting', { enabled: false }); -} - submodule('userId', id5IdSubmodule); diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md index 11f8ffc5609..cf90290b1d8 100644 --- a/modules/id5IdSystem.md +++ b/modules/id5IdSystem.md @@ -29,7 +29,8 @@ pbjs.setConfig({ abTesting: { // optional enabled: true, // false by default controlGroupPct: 0.1 // valid values are 0.0 - 1.0 (inclusive) - } + }, + disableExtensions: false // optional }, storage: { type: 'html5', // "html5" is the required storage type @@ -43,21 +44,22 @@ pbjs.setConfig({ }); ``` -| Param under userSync.userIds[] | Scope | Type | Description | Example | +| Param under userSync.userIds[] | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | -| name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | -| params | Required | Object | Details for the ID5 ID. | | -| params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | -| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://support.id5.io/portal/en/kb/articles/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | -| params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | -| params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default | -| params.abTesting.enabled | Optional | Boolean | Set this to `true` to turn on this feature | `true` or `false` | +| name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | +| params | Required | Object | Details for the ID5 ID. | | +| params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | +| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://support.id5.io/portal/en/kb/articles/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | +| params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | +| params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default | +| params.abTesting.enabled | Optional | Boolean | Set this to `true` to turn on this feature | `true` or `false` | | params.abTesting.controlGroupPct | Optional | Number | Must be a number between `0.0` and `1.0` (inclusive) and is used to determine the percentage of requests that fall into the control group (and thus not exposing the ID5 ID). For example, a value of `0.20` will result in 20% of requests without an ID5 ID and 80% with an ID. | `0.1` | -| storage | Required | Object | Storage settings for how the User ID module will cache the ID5 ID locally | | -| storage.type | Required | String | This is where the results of the user ID will be stored. ID5 **requires** `"html5"`. | `"html5"` | -| storage.name | Required | String | The name of the local storage where the user ID will be stored. ID5 **requires** `"id5id"`. | `"id5id"` | -| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. ID5 recommends `90`. | `90` | -| storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 8 hours between refreshes | `8*3600` | +| params.disableExtensions | Optional | Boolean | Set this to `true` to force turn off extensions call. Default `false` | `true` or `false` | +| storage | Required | Object | Storage settings for how the User ID module will cache the ID5 ID locally | | +| storage.type | Required | String | This is where the results of the user ID will be stored. ID5 **requires** `"html5"`. | `"html5"` | +| storage.name | Required | String | The name of the local storage where the user ID will be stored. ID5 **requires** `"id5id"`. | `"id5id"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. ID5 recommends `90`. | `90` | +| storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 8 hours between refreshes | `8*3600` | **ATTENTION:** As of Prebid.js v4.14.0, ID5 requires `storage.type` to be `"html5"` and `storage.name` to be `"id5id"`. Using other values will display a warning today, but in an upcoming release, it will prevent the ID5 module from loading. This change is to ensure the ID5 module in Prebid.js interoperates properly with the [ID5 API](https://github.com/id5io/id5-api.js) and to reduce the size of publishers' first-party cookies that are sent to their web servers. If you have any questions, please reach out to us at [prebid@id5.io](mailto:prebid@id5.io). diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index a54542f7278..8c0f8ad9cf3 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -5,28 +5,36 @@ import { ID5_PRIVACY_STORAGE_NAME, ID5_STORAGE_NAME, id5IdSubmodule, - nbCacheName, storage, + nbCacheName, + storage, storeInLocalStorage, storeNbInCache, } from 'modules/id5IdSystem.js'; import {coreStorage, init, requestBidsHook, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; -import {server} from 'test/mocks/xhr.js'; import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; +import {uspDataHandler} from 'src/adapterManager.js'; import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; let expect = require('chai').expect; -describe('ID5 ID System', function() { +describe('ID5 ID System', function () { const ID5_MODULE_NAME = 'id5Id'; const ID5_EIDS_NAME = ID5_MODULE_NAME.toLowerCase(); const ID5_SOURCE = 'id5-sync.com'; const ID5_TEST_PARTNER_ID = 173; const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_TEST_PARTNER_ID}.json`; + const ID5_API_CONFIG_URL = `https://id5-sync.com/api/config/prebid`; + const ID5_EXTENSIONS_ENDPOINT = 'https://extensions.id5-sync.com/test'; + const ID5_API_CONFIG = { + fetchCall: { + url: ID5_ENDPOINT + } + }; const ID5_NB_STORAGE_NAME = nbCacheName(ID5_TEST_PARTNER_ID); const ID5_STORED_ID = 'storedid5id'; const ID5_STORED_SIGNATURE = '123456'; @@ -58,6 +66,7 @@ describe('ID5 ID System', function() { } } } + function getId5ValueConfig(value) { return { name: ID5_MODULE_NAME, @@ -68,6 +77,7 @@ describe('ID5 ID System', function() { } } } + function getUserSyncConfig(userIds) { return { userSync: { @@ -76,12 +86,15 @@ describe('ID5 ID System', function() { } } } + function getFetchLocalStorageConfig() { return getUserSyncConfig([getId5FetchConfig(ID5_STORAGE_NAME, 'html5')]); } + function getValueConfig(value) { return getUserSyncConfig([getId5ValueConfig(value)]); } + function getAdUnitMock(code = 'adUnit-code') { return { code, @@ -91,15 +104,81 @@ describe('ID5 ID System', function() { }; } + function callSubmoduleGetId(config, consentData, cacheIdObj) { + return new Promise((resolve) => { + id5IdSubmodule.getId(config, consentData, cacheIdObj).callback((response) => { + resolve(response) + }) + }); + } + + class XhrServerMock { + constructor(server) { + this.currentRequestIdx = 0 + this.server = server + } + + expectFirstRequest() { + return this.#expectRequest(0); + } + + expectNextRequest() { + return this.#expectRequest(++this.currentRequestIdx) + } + + expectConfigRequest() { + return this.expectFirstRequest() + .then(configRequest => { + expect(configRequest.url).is.eq(ID5_API_CONFIG_URL); + expect(configRequest.method).is.eq('POST'); + return configRequest; + }) + } + + respondWithConfigAndExpectNext(configRequest, config = ID5_API_CONFIG) { + configRequest.respond(200, {'Content-Type': 'application/json'}, JSON.stringify(config)); + return this.expectNextRequest() + } + + expectFetchRequest() { + return this.expectConfigRequest() + .then(configRequest => { + return this.respondWithConfigAndExpectNext(configRequest, ID5_API_CONFIG); + }).then(request => { + expect(request.url).is.eq(ID5_API_CONFIG.fetchCall.url); + expect(request.method).is.eq('POST'); + return request; + }) + } + + #expectRequest(index) { + let server = this.server + return new Promise(function (resolve) { + (function waitForCondition() { + if (server.requests && server.requests.length > index) return resolve(server.requests[index]); + setTimeout(waitForCondition, 30); + })(); + }) + .then(request => { + return request + }); + } + + hasReceivedAnyRequest() { + let requests = this.server.requests; + return requests && requests.length > 0; + } + } + before(() => { hook.ready(); }); - describe('Check for valid publisher config', function() { - it('should fail with invalid config', function() { + describe('Check for valid publisher config', function () { + it('should fail with invalid config', function () { // no config - expect(id5IdSubmodule.getId()).to.be.eq(undefined); - expect(id5IdSubmodule.getId({ })).to.be.eq(undefined); + expect(id5IdSubmodule.getId()).is.eq(undefined); + expect(id5IdSubmodule.getId({})).is.eq(undefined); // valid params, invalid storage expect(id5IdSubmodule.getId({ params: { partner: 123 } })).to.be.eq(undefined); @@ -113,7 +192,7 @@ describe('ID5 ID System', function() { expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 'abc' } })).to.be.eq(undefined); }); - it('should warn with non-recommended storage params', function() { + it('should warn with non-recommended storage params', function () { let logWarnStub = sinon.stub(utils, 'logWarn'); id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 123 } }); @@ -126,189 +205,457 @@ describe('ID5 ID System', function() { }); }); - describe('Xhr Requests from getId()', function() { - const responseHeader = { 'Content-Type': 'application/json' }; - let callbackSpy = sinon.spy(); + describe('Xhr Requests from getId()', function () { + const responseHeader = {'Content-Type': 'application/json'}; - beforeEach(function() { - callbackSpy.resetHistory(); + beforeEach(function () { }); - afterEach(function () { + afterEach(function () { + uspDataHandler.reset() }); it('should call the ID5 server and handle a valid response', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; - expect(requestBody.partner).to.eq(ID5_TEST_PARTNER_ID); - expect(requestBody.o).to.eq('pbjs'); - expect(requestBody.pd).to.be.undefined; - expect(requestBody.s).to.be.undefined; - expect(requestBody.provider).to.be.undefined - expect(requestBody.v).to.eq('$prebid.version$'); - expect(requestBody.gdpr).to.exist; - expect(requestBody.gdpr_consent).to.be.undefined; - expect(requestBody.us_privacy).to.be.undefined; - - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let config = getId5FetchConfig(); + let submoduleResponse = callSubmoduleGetId(config, undefined, undefined); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(fetchRequest.url).to.contain(ID5_ENDPOINT); + expect(fetchRequest.withCredentials).is.true; + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.o).is.eq('pbjs'); + expect(requestBody.pd).is.undefined; + expect(requestBody.s).is.undefined; + expect(requestBody.provider).is.undefined + expect(requestBody.v).is.eq('$prebid.version$'); + expect(requestBody.gdpr).is.eq(0); + expect(requestBody.gdpr_consent).is.undefined; + expect(requestBody.us_privacy).is.undefined; + expect(requestBody.storage).is.deep.eq(config.storage) + + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(submoduleResponse => { + expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + }); + }); + + it('should call the ID5 server with gdpr data ', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let consentData = { + gdprApplies: true, + consentString: 'consentString' + } + + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), consentData, undefined); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.gdpr).to.eq(1); + expect(requestBody.gdpr_consent).is.eq(consentData.consentString); + + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(submoduleResponse => { + expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + }); + }); + + it('should call the ID5 server without gdpr data when gdpr not applies ', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let consentData = { + gdprApplies: false, + consentString: 'consentString' + } + + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), consentData, undefined); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.gdpr).to.eq(0); + expect(requestBody.gdpr_consent).is.undefined + + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(submoduleResponse => { + expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + }); + }); + + it('should call the ID5 server with us privacy consent', function () { + let usPrivacyString = '1YN-'; + uspDataHandler.setConsentData(usPrivacyString) + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let consentData = { + gdprApplies: true, + consentString: 'consentString' + } + + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), consentData, undefined); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.us_privacy).to.eq(usPrivacyString); + + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(submoduleResponse => { + expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + }); }); it('should call the ID5 server with no signature field when no stored object', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback; - submoduleCallback(callbackSpy); + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.s).is.undefined; + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + }); - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.s).to.be.undefined; + it('should call the ID5 server for config with submodule config object', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let id5FetchConfig = getId5FetchConfig(); + id5FetchConfig.params.extraParam = { + x: 'X', + y: { + a: 1, + b: '3' + } + } + let submoduleResponse = callSubmoduleGetId(id5FetchConfig, undefined, undefined); + + return xhrServerMock.expectConfigRequest() + .then(configRequest => { + let requestBody = JSON.parse(configRequest.requestBody) + expect(requestBody).is.deep.eq(id5FetchConfig) + return xhrServerMock.respondWithConfigAndExpectNext(configRequest) + }) + .then(fetchRequest => { + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + }); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + it('should call the ID5 server for config with overridden url', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let id5FetchConfig = getId5FetchConfig(); + id5FetchConfig.params.configUrl = 'http://localhost/x/y/z' + + let submoduleResponse = callSubmoduleGetId(id5FetchConfig, undefined, undefined); + + return xhrServerMock.expectFirstRequest() + .then(configRequest => { + expect(configRequest.url).is.eq('http://localhost/x/y/z') + return xhrServerMock.respondWithConfigAndExpectNext(configRequest) + }) + .then(fetchRequest => { + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) }); - it('should call the ID5 server with signature field from stored object', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); + it('should call the ID5 server with additional data when provided', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); + + return xhrServerMock.expectConfigRequest() + .then(configRequest => { + return xhrServerMock.respondWithConfigAndExpectNext(configRequest, { + fetchCall: { + url: ID5_ENDPOINT, + overrides: { + arg1: '123', + arg2: { + x: '1', + y: 2 + } + } + } + }); + }) + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.o).is.eq('pbjs'); + expect(requestBody.v).is.eq('$prebid.version$'); + expect(requestBody.arg1).is.eq('123') + expect(requestBody.arg2).is.deep.eq({ + x: '1', + y: 2 + }) + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + }); - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE); + it('should call the ID5 server with extensions', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); + + return xhrServerMock.expectConfigRequest() + .then(configRequest => { + return xhrServerMock.respondWithConfigAndExpectNext(configRequest, { + fetchCall: { + url: ID5_ENDPOINT + }, + extensionsCall: { + url: ID5_EXTENSIONS_ENDPOINT, + method: 'GET' + } + }); + }) + .then(extensionsRequest => { + expect(extensionsRequest.url).is.eq(ID5_EXTENSIONS_ENDPOINT) + expect(extensionsRequest.method).is.eq('GET') + extensionsRequest.respond(200, responseHeader, JSON.stringify({ + lb: 'ex' + })) + return xhrServerMock.expectNextRequest(); + }) + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.o).is.eq('pbjs'); + expect(requestBody.v).is.eq('$prebid.version$'); + expect(requestBody.extensions).is.deep.eq({ + lb: 'ex' + }) + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + }); + + it('should call the ID5 server with extensions fetched with POST', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); + + return xhrServerMock.expectConfigRequest() + .then(configRequest => { + return xhrServerMock.respondWithConfigAndExpectNext(configRequest, { + fetchCall: { + url: ID5_ENDPOINT + }, + extensionsCall: { + url: ID5_EXTENSIONS_ENDPOINT, + method: 'POST', + body: { + x: '1', + y: 2 + } + } + }); + }) + .then(extensionsRequest => { + expect(extensionsRequest.url).is.eq(ID5_EXTENSIONS_ENDPOINT) + expect(extensionsRequest.method).is.eq('POST') + let requestBody = JSON.parse(extensionsRequest.requestBody) + expect(requestBody).is.deep.eq({ + x: '1', + y: 2 + }) + extensionsRequest.respond(200, responseHeader, JSON.stringify({ + lb: 'post', + })) + return xhrServerMock.expectNextRequest(); + }) + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.o).is.eq('pbjs'); + expect(requestBody.v).is.eq('$prebid.version$'); + expect(requestBody.extensions).is.deep.eq({ + lb: 'post' + }) + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + }); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + it('should call the ID5 server with signature field from stored object', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.s).is.eq(ID5_STORED_SIGNATURE); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) }); it('should call the ID5 server with pd field when pd config is set', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) const pubData = 'b50ca08271795a8e7e4012813f23d505193d75c0f2e2bb99baa63aa822f66ed3'; let id5Config = getId5FetchConfig(); id5Config.params.pd = pubData; - let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.pd).to.eq(pubData); + let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.pd).is.eq(pubData); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse; + }) }); it('should call the ID5 server with no pd field when pd config is not set', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) let id5Config = getId5FetchConfig(); id5Config.params.pd = undefined; - let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); + let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.pd).to.be.undefined; - - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.pd).is.undefined; + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse; + }) }); it('should call the ID5 server with nb=1 when no stored value exists and reset after', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.nbPage).to.eq(1); - - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.nbPage).is.eq(1); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(() => { + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(0); + }) }); it('should call the ID5 server with incremented nb when stored value exists and reset after', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) storeNbInCache(ID5_TEST_PARTNER_ID, 1); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.nbPage).to.eq(2); - - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.nbPage).is.eq(2); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(() => { + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(0); + }) }); it('should call the ID5 server with ab_testing object when abTesting is turned on', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) let id5Config = getId5FetchConfig(); - id5Config.params.abTesting = { enabled: true, controlGroupPct: 0.234 } - - let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); + id5Config.params.abTesting = {enabled: true, controlGroupPct: 0.234} - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.ab_testing.enabled).to.eq(true); - expect(requestBody.ab_testing.control_group_pct).to.eq(0.234); + let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.ab_testing.enabled).is.eq(true); + expect(requestBody.ab_testing.control_group_pct).is.eq(0.234); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse; + }); }); it('should call the ID5 server without ab_testing object when abTesting is turned off', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) let id5Config = getId5FetchConfig(); - id5Config.params.abTesting = { enabled: false, controlGroupPct: 0.55 } - - let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); + id5Config.params.abTesting = {enabled: false, controlGroupPct: 0.55} - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.ab_testing).to.be.undefined; + let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.ab_testing).is.undefined; + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }); }); it('should call the ID5 server without ab_testing when when abTesting is not set', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) let id5Config = getId5FetchConfig(); - let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.ab_testing).to.be.undefined; + let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.ab_testing).is.undefined; + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }); }); it('should store the privacy object from the ID5 server response', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); - let request = server.requests[0]; - - let responseObject = utils.deepClone(ID5_JSON_RESPONSE); - responseObject.privacy = { + const privacy = { jurisdiction: 'gdpr', id5_consent: true }; - request.respond(200, responseHeader, JSON.stringify(responseObject)); - expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).to.be.eq(JSON.stringify(responseObject.privacy)); - coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME); + + return xhrServerMock.expectFetchRequest() + .then(request => { + let responseObject = utils.deepClone(ID5_JSON_RESPONSE); + responseObject.privacy = privacy; + request.respond(200, responseHeader, JSON.stringify(responseObject)); + return submoduleResponse + }) + .then(() => { + expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).is.eq(JSON.stringify(privacy)); + coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME); + }) }); it('should not store a privacy object if not part of ID5 server response', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).to.be.null; + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + + return xhrServerMock.expectFetchRequest() + .then(request => { + let responseObject = utils.deepClone(ID5_JSON_RESPONSE); + responseObject.privacy = undefined; + request.respond(200, responseHeader, JSON.stringify(responseObject)); + return submoduleResponse + }) + .then(() => { + expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).is.null; + }); }); describe('when legacy cookies are set', () => { @@ -327,11 +674,11 @@ describe('ID5 ID System', function() { }) }); - describe('Request Bids Hook', function() { + describe('Request Bids Hook', function () { let adUnits; let sandbox; - beforeEach(function() { + beforeEach(function () { sandbox = sinon.sandbox.create(); mockGdprConsent(sandbox); sinon.stub(events, 'getEvents').returns([]); @@ -340,7 +687,7 @@ describe('ID5 ID System', function() { coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); adUnits = [getAdUnitMock()]; }); - afterEach(function() { + afterEach(function () { events.getEvents.restore(); coreStorage.removeDataFromLocalStorage(ID5_STORAGE_NAME); coreStorage.removeDataFromLocalStorage(`${ID5_STORAGE_NAME}_last`); @@ -359,8 +706,8 @@ describe('ID5 ID System', function() { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); - expect(bid.userId.id5id.uid).to.equal(ID5_STORED_ID); - expect(bid.userIdAsEids[0]).to.deep.equal({ + expect(bid.userId.id5id.uid).is.equal(ID5_STORED_ID); + expect(bid.userIdAsEids[0]).is.deep.equal({ source: ID5_SOURCE, uids: [{ id: ID5_STORED_ID, @@ -373,7 +720,7 @@ describe('ID5 ID System', function() { }); }); done(); - }, { adUnits }); + }, {adUnits}); }); it('should add config value ID to bids', function (done) { @@ -385,15 +732,15 @@ describe('ID5 ID System', function() { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); - expect(bid.userId.id5id.uid).to.equal(ID5_STORED_ID); - expect(bid.userIdAsEids[0]).to.deep.equal({ + expect(bid.userId.id5id.uid).is.equal(ID5_STORED_ID); + expect(bid.userIdAsEids[0]).is.deep.equal({ source: ID5_SOURCE, - uids: [{ id: ID5_STORED_ID, atype: 1 }] + uids: [{id: ID5_STORED_ID, atype: 1}] }); }); }); done(); - }, { adUnits }); + }, {adUnits}); }); it('should set nb=1 in cache when no stored nb value exists and cached ID', function (done) { @@ -405,7 +752,7 @@ describe('ID5 ID System', function() { config.setConfig(getFetchLocalStorageConfig()); requestBidsHook((adUnitConfig) => { - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(1); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(1); done() }, {adUnits}); }); @@ -419,19 +766,20 @@ describe('ID5 ID System', function() { config.setConfig(getFetchLocalStorageConfig()); requestBidsHook(() => { - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(2); done() }, {adUnits}); }); it('should call ID5 servers with signature and incremented nb post auction if refresh needed', function () { - storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let initialLocalStorageValue = JSON.stringify(ID5_STORED_OBJ); + storeInLocalStorage(ID5_STORAGE_NAME, initialLocalStorageValue, 1); storeInLocalStorage(`${ID5_STORAGE_NAME}_last`, expDaysStr(-1), 1); - storeNbInCache(ID5_TEST_PARTNER_ID, 1); + storeNbInCache(ID5_TEST_PARTNER_ID, 1); let id5Config = getFetchLocalStorageConfig(); id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; - init(config); setSubmoduleRegistry([id5IdSubmodule]); config.setConfig(id5Config); @@ -441,53 +789,62 @@ describe('ID5 ID System', function() { resolve() }, {adUnits}); }).then(() => { - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); - expect(server.requests).to.be.empty; + expect(xhrServerMock.hasReceivedAnyRequest()).is.false; events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); - return new Promise((resolve) => setTimeout(resolve)) - }).then(() => { - let request = server.requests[0]; + return xhrServerMock.expectFetchRequest() + }).then(request => { let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE); - expect(requestBody.nbPage).to.eq(2); - - const responseHeader = { 'Content-Type': 'application/json' }; + expect(requestBody.s).is.eq(ID5_STORED_SIGNATURE); + expect(requestBody.nbPage).is.eq(2); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(2); + const responseHeader = {'Content-Type': 'application/json'}; request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(decodeURIComponent(getFromLocalStorage(ID5_STORAGE_NAME))).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); + return new Promise(function (resolve) { + (function waitForCondition() { + if (getFromLocalStorage(ID5_STORAGE_NAME) !== initialLocalStorageValue) return resolve(); + setTimeout(waitForCondition, 30); + })(); + }) + }).then(() => { + expect(decodeURIComponent(getFromLocalStorage(ID5_STORAGE_NAME))).is.eq(JSON.stringify(ID5_JSON_RESPONSE)); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(0); }) }); }); - describe('Decode stored object', function() { - const expectedDecodedObject = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } }; + describe('Decode stored object', function () { + const expectedDecodedObject = {id5id: {uid: ID5_STORED_ID, ext: {linkType: ID5_STORED_LINK_TYPE}}}; - it('should properly decode from a stored object', function() { - expect(id5IdSubmodule.decode(ID5_STORED_OBJ, getId5FetchConfig())).to.deep.equal(expectedDecodedObject); + it('should properly decode from a stored object', function () { + expect(id5IdSubmodule.decode(ID5_STORED_OBJ, getId5FetchConfig())).is.deep.equal(expectedDecodedObject); }); - it('should return undefined if passed a string', function() { - expect(id5IdSubmodule.decode('somestring', getId5FetchConfig())).to.eq(undefined); + it('should return undefined if passed a string', function () { + expect(id5IdSubmodule.decode('somestring', getId5FetchConfig())).is.eq(undefined); }); }); - describe('A/B Testing', function() { - const expectedDecodedObjectWithIdAbOff = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } }; - const expectedDecodedObjectWithIdAbOn = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE, abTestingControlGroup: false } } }; - const expectedDecodedObjectWithoutIdAbOn = { id5id: { uid: '', ext: { linkType: 0, abTestingControlGroup: true } } }; + describe('A/B Testing', function () { + const expectedDecodedObjectWithIdAbOff = {id5id: {uid: ID5_STORED_ID, ext: {linkType: ID5_STORED_LINK_TYPE}}}; + const expectedDecodedObjectWithIdAbOn = { + id5id: { + uid: ID5_STORED_ID, + ext: {linkType: ID5_STORED_LINK_TYPE, abTestingControlGroup: false} + } + }; + const expectedDecodedObjectWithoutIdAbOn = {id5id: {uid: '', ext: {linkType: 0, abTestingControlGroup: true}}}; let testConfig, storedObject; - beforeEach(function() { + beforeEach(function () { testConfig = getId5FetchConfig(); storedObject = utils.deepClone(ID5_STORED_OBJ); }); - describe('A/B Testing Config is Set', function() { + describe('A/B Testing Config is Set', function () { let randStub; - beforeEach(function() { - randStub = sinon.stub(Math, 'random').callsFake(function() { + beforeEach(function () { + randStub = sinon.stub(Math, 'random').callsFake(function () { return 0.25; }); }); @@ -495,39 +852,39 @@ describe('ID5 ID System', function() { randStub.restore(); }); - describe('Decode', function() { + describe('Decode', function () { let logErrorSpy; - beforeEach(function() { + beforeEach(function () { logErrorSpy = sinon.spy(utils, 'logError'); }); - afterEach(function() { + afterEach(function () { logErrorSpy.restore(); }); it('should not set abTestingControlGroup extension when A/B testing is off', function () { let decoded = id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + expect(decoded).is.deep.equal(expectedDecodedObjectWithIdAbOff); }); it('should set abTestingControlGroup to false when A/B testing is on but in normal group', function () { - storedObject.ab_testing = { result: 'normal' }; + storedObject.ab_testing = {result: 'normal'}; let decoded = id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOn); + expect(decoded).is.deep.equal(expectedDecodedObjectWithIdAbOn); }); it('should not expose ID when everyone is in control group', function () { - storedObject.ab_testing = { result: 'control' }; + storedObject.ab_testing = {result: 'control'}; storedObject.universal_uid = ''; storedObject.link_type = 0; let decoded = id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).to.deep.equal(expectedDecodedObjectWithoutIdAbOn); + expect(decoded).is.deep.equal(expectedDecodedObjectWithoutIdAbOn); }); it('should log A/B testing errors', function () { - storedObject.ab_testing = { result: 'error' }; + storedObject.ab_testing = {result: 'error'}; let decoded = id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + expect(decoded).is.deep.equal(expectedDecodedObjectWithIdAbOff); sinon.assert.calledOnce(logErrorSpy); }); }); From 4a3daec6f1854f1b323140b84e8edb8e93d4b699 Mon Sep 17 00:00:00 2001 From: JacobKlein26 <42449375+JacobKlein26@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:28:54 -0400 Subject: [PATCH 10/25] NextMillenium Bid Adapter: Remove ortb2 referrerInfo (#8868) * remove ortb2, get device/site manually * updated tests * remove fallbacks * no need to craete variable if there is no fallback (return in place) * removed one test case Co-authored-by: Yakov Klein --- modules/nextMillenniumBidAdapter.js | 31 +++++++++++++++---- .../modules/nextMillenniumBidAdapter_spec.js | 9 ++---- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index f873e5b5c29..87d2e53568f 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -26,11 +26,12 @@ export const spec = { _each(validBidRequests, function(bid) { window.nmmRefreshCounts[bid.adUnitCode] = window.nmmRefreshCounts[bid.adUnitCode] || 0; const id = getPlacementId(bid) + let sizes = bid.sizes + if (sizes && !Array.isArray(sizes[0])) sizes = [sizes] + + const site = getSiteObj() + const device = getDeviceObj() - if (bid.sizes && !Array.isArray(bid.sizes[0])) bid.sizes = [bid.sizes] - if (!bid.ortb2) bid.ortb2 = {} - if (!bid.ortb2.device) bid.ortb2.device = {} - bid.ortb2.device.referrer = (getRefererInfo && getRefererInfo().ref) || '' const postBody = { 'id': bid.auctionId, 'ext': { @@ -46,10 +47,11 @@ export const spec = { 'scrollTop': window.pageYOffset || document.documentElement.scrollTop } }, - ...bid.ortb2, + device, + site, 'imp': [{ 'banner': { - 'format': (bid.sizes || []).map(s => { return {w: s[0], h: s[1]} }) + 'format': (sizes || []).map(s => { return {w: s[0], h: s[1]} }) }, 'ext': { 'prebid': { @@ -198,4 +200,21 @@ function getTopWindow(curWindow, nesting = 0) { } } +function getSiteObj() { + const refInfo = (getRefererInfo && getRefererInfo()) || {} + + return { + page: refInfo.page, + ref: refInfo.ref, + domain: refInfo.domain + } +} + +function getDeviceObj() { + return { + w: window.innerWidth || window.document.documentElement.clientWidth || window.document.body.clientWidth || 0, + h: window.innerHeight || window.document.documentElement.clientHeight || window.document.body.clientHeight || 0, + } +} + registerBidder(spec); diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index 40005356fd8..303025888dc 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -104,9 +104,9 @@ describe('nextMillenniumBidAdapterTests', function() { expect(JSON.parse(request[0].data).ext.nextMillennium.refresh_count).to.equal(3); }); - it('Check if ORTB was added', function() { + it('Check if domain was added', function() { const request = spec.buildRequests(bidRequestData) - expect(JSON.parse(request[0].data).site.domain).to.equal('example.com') + expect(JSON.parse(request[0].data).site.domain).to.exist }) it('Check if elOffsets was added', function() { @@ -114,11 +114,6 @@ describe('nextMillenniumBidAdapterTests', function() { expect(JSON.parse(request[0].data).ext.nextMillennium.elOffsets).to.be.an('object') }) - it('Check if refferer was added', function() { - const request = spec.buildRequests(bidRequestData) - expect(JSON.parse(request[0].data).device.referrer).to.exist - }) - it('Check if imp object was added', function() { const request = spec.buildRequests(bidRequestData) expect(JSON.parse(request[0].data).imp).to.be.an('array') From 6f0a824d40d706d044b9b4c0070aed0c04bd5942 Mon Sep 17 00:00:00 2001 From: caseywhitmire <60086994+caseywhitmire@users.noreply.github.com> Date: Fri, 19 Aug 2022 08:26:29 -0700 Subject: [PATCH 11/25] adserver.js : remove unused code (#8855) --- src/adserver.js | 56 ------------------------------------------------- 1 file changed, 56 deletions(-) diff --git a/src/adserver.js b/src/adserver.js index 8d99b29c3ef..db7aaaa1dc8 100644 --- a/src/adserver.js +++ b/src/adserver.js @@ -1,61 +1,5 @@ -import { formatQS } from './utils.js'; -import { targeting } from './targeting.js'; import {hook} from './hook.js'; -// Adserver parent class -const AdServer = function(attr) { - this.name = attr.adserver; - this.code = attr.code; - this.getWinningBidByCode = function() { - return targeting.getWinningBids(this.code)[0]; - }; -}; - -// DFP ad server -// TODO: this seems to be unused? -export function dfpAdserver(options, urlComponents) { - var adserver = new AdServer(options); - adserver.urlComponents = urlComponents; - - var dfpReqParams = { - 'env': 'vp', - 'gdfp_req': '1', - 'impl': 's', - 'unviewed_position_start': '1' - }; - - var dfpParamsWithVariableValue = ['output', 'iu', 'sz', 'url', 'correlator', 'description_url', 'hl']; - - var getCustomParams = function(targeting) { - return encodeURIComponent(formatQS(targeting)); - }; - - adserver.appendQueryParams = function() { - var bid = adserver.getWinningBidByCode(); - if (bid) { - this.urlComponents.search.description_url = encodeURIComponent(bid.vastUrl); - this.urlComponents.search.cust_params = getCustomParams(bid.adserverTargeting); - this.urlComponents.search.correlator = Date.now(); - } - }; - - adserver.verifyAdserverTag = function() { - for (var key in dfpReqParams) { - if (!this.urlComponents.search.hasOwnProperty(key) || this.urlComponents.search[key] !== dfpReqParams[key]) { - return false; - } - } - for (var i in dfpParamsWithVariableValue) { - if (!this.urlComponents.search.hasOwnProperty(dfpParamsWithVariableValue[i])) { - return false; - } - } - return true; - }; - - return adserver; -}; - /** * return the GAM PPID, if available (eid for the userID configured with `userSync.ppidSource`) */ From 3a029f684ee3992233e4de1cfa5b1cfbe19d6667 Mon Sep 17 00:00:00 2001 From: philan15 <37775368+philan15@users.noreply.github.com> Date: Fri, 19 Aug 2022 19:21:46 +0300 Subject: [PATCH 12/25] Displayio Bid Adapter: custom render; fix eids payload (#8847) * Custom render; call pubmatic get user id function is removed * use refererInfo; remove call createEidsArray --- modules/displayioBidAdapter.js | 189 +++++++++--------- test/spec/modules/displayioBidAdapter_spec.js | 68 ++----- 2 files changed, 108 insertions(+), 149 deletions(-) diff --git a/modules/displayioBidAdapter.js b/modules/displayioBidAdapter.js index e039d461fc7..c3c6597dd1b 100644 --- a/modules/displayioBidAdapter.js +++ b/modules/displayioBidAdapter.js @@ -1,16 +1,16 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {Renderer} from '../src/Renderer.js'; +import {getWindowFromDocument, logWarn} from '../src/utils.js'; -const BIDDER_VERSION = '1.0.0'; +const ADAPTER_VERSION = '1.1.0'; const BIDDER_CODE = 'displayio'; -const GVLID = 999; const BID_TTL = 300; const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const DEFAULT_CURRENCY = 'USD'; export const spec = { code: BIDDER_CODE, - gvlid: GVLID, supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function(bid) { return !!(bid.params && bid.params.placementId && bid.params.siteId && @@ -20,7 +20,7 @@ export const spec = { return bidRequests.map(bid => { let url = '//' + bid.params.adsSrvDomain + '/srv?method=getPlacement&app=' + bid.params.siteId + '&placement=' + bid.params.placementId; - const data = this._getPayload(bid, bidderRequest); + const data = getPayload(bid, bidderRequest); return { method: 'POST', headers: {'Content-Type': 'application/json;charset=utf-8'}, @@ -42,117 +42,112 @@ export const spec = { height: adData.h, netRevenue: true, ttl: BID_TTL, - creativeId: adData.adId || 0, - currency: DEFAULT_CURRENCY, + creativeId: adData.adId || 1, + currency: adData.cur || DEFAULT_CURRENCY, referrer: data.data.ref, - mediaType: ads[0].ad.subtype, + mediaType: ads[0].ad.subtype === 'videoVast' ? VIDEO : BANNER, ad: adData.markup, - placement: data.placement, + adUnitCode: data.adUnitCode, + renderURL: data.renderURL, adData: adData }; - if (bidResponse.vastUrl === 'videoVast') { - bidResponse.vastUrl = adData.videos[0].url + + if (bidResponse.mediaType === VIDEO) { + bidResponse.vastUrl = adData.videos[0] && adData.videos[0].url + } + + if (bidResponse.renderURL) { + bidResponse.renderer = newRenderer(bidResponse); } bidResponses.push(bidResponse); } return bidResponses; - }, - _getPayload: function (bid, bidderRequest) { - const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; - const userSession = 'us_web_xxxxxxxxxxxx'.replace(/[x]/g, c => { - let r = Math.random() * 16 | 0; - let v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - const { params } = bid; - const { siteId, placementId } = params; - const { refererInfo, uspConsent, gdprConsent } = bidderRequest; - const mediation = {consent: '-1', gdpr: '-1'}; - if (gdprConsent) { - if (gdprConsent.consentString !== undefined) { - mediation.consent = gdprConsent.consentString; - } - if (gdprConsent.gdprApplies !== undefined) { - mediation.gdpr = gdprConsent.gdprApplies ? '1' : '0'; - } + } +}; + +function getPayload (bid, bidderRequest) { + const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; + const userSession = 'us_web_xxxxxxxxxxxx'.replace(/[x]/g, c => { + let r = Math.random() * 16 | 0; + let v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + const { params, adUnitCode, bidId } = bid; + const { siteId, placementId, renderURL, pageCategory, keywords } = params; + const { refererInfo, uspConsent, gdprConsent } = bidderRequest; + const mediation = {consent: '-1', gdpr: '-1'}; + if (gdprConsent && 'gdprApplies' in gdprConsent) { + if (gdprConsent.consentString !== undefined) { + mediation.consent = gdprConsent.consentString; } - const payload = { - userSession, + if (gdprConsent.gdprApplies !== undefined) { + mediation.gdpr = gdprConsent.gdprApplies ? '1' : '0'; + } + } + return { + userSession, + data: { + id: bidId, + action: 'getPlacement', + app: siteId, + placement: placementId, + adUnitCode, + renderURL, data: { - id: bid.bidId, - action: 'getPlacement', - app: siteId, - placement: placementId, - data: { - pagecat: params.pageCategory ? params.pageCategory.split(',').map(k => k.trim()) : [], - keywords: params.keywords ? params.keywords.split(',').map(k => k.trim()) : [], - lang_content: document.documentElement.lang, - lang: window.navigator.language, - // TODO: are these the correct refererInfo values? - domain: refererInfo.domain, - page: refererInfo.page, - ref: refererInfo.ref, - userids: _getUserIDs(), - geo: '', - }, - complianceData: { - child: '-1', - us_privacy: uspConsent, - dnt: window.navigator.doNotTrack, - iabConsent: {}, - mediation: { - consent: mediation.consent, - gdpr: mediation.gdpr, - } - }, - integration: 'JS', - omidpn: 'Displayio', - mediationPlatform: 0, - prebidVersion: BIDDER_VERSION, - device: { - w: window.screen.width, - h: window.screen.height, - connection_type: connection ? connection.effectiveType : '', + pagecat: pageCategory ? pageCategory.split(',').map(k => k.trim()) : [], + keywords: keywords ? keywords.split(',').map(k => k.trim()) : [], + lang_content: document.documentElement.lang, + lang: window.navigator.language, + domain: refererInfo.domain, + page: refererInfo.page, + ref: refererInfo.referer, + userids: bid.userIdAsEids || {}, + geo: '', + }, + complianceData: { + child: '-1', + us_privacy: uspConsent, + dnt: window.doNotTrack === '1' || window.navigator.doNotTrack === '1' || false, + iabConsent: {}, + mediation: { + consent: mediation.consent, + gdpr: mediation.gdpr, } + }, + integration: 'JS', + omidpn: 'Displayio', + mediationPlatform: 0, + prebidVersion: ADAPTER_VERSION, + device: { + w: window.screen.width, + h: window.screen.height, + connection_type: connection ? connection.effectiveType : '', } } - if (navigator.permissions) { - navigator.permissions.query({ name: 'geolocation' }) - .then((result) => { - if (result.state === 'granted') { - payload.data.data.geo = _getGeoData(); - } - }); - } - return payload } -}; +} + +function newRenderer(bid) { + const renderer = Renderer.install({ + id: bid.requestId, + url: bid.renderURL, + adUnitCode: bid.adUnitCode + }); -function _getUserIDs () { - let ids = {}; try { - ids = window.owpbjs.getUserIdsAsEids(); - } catch (e) {} - return ids; + renderer.setRender(webisRender); + } catch (err) { + logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; } -async function _getGeoData () { - let geoData = null; - const getCurrentPosition = () => { - return new Promise((resolve, reject) => - navigator.geolocation.getCurrentPosition(resolve, reject) - ); - } - try { - const position = await getCurrentPosition(); - let {latitude, longitude, accuracy} = position.coords; - geoData = { - 'lat': latitude, - 'lng': longitude, - 'precision': accuracy - }; - } catch (e) {} - return geoData +function webisRender(bid, doc) { + bid.renderer.push(() => { + const win = getWindowFromDocument(doc) || window; + win.webis.init(bid.adData, bid.adUnitCode, bid.params); + }) } registerBidder(spec); diff --git a/test/spec/modules/displayioBidAdapter_spec.js b/test/spec/modules/displayioBidAdapter_spec.js index dfeb67fb467..56b8b85384b 100644 --- a/test/spec/modules/displayioBidAdapter_spec.js +++ b/test/spec/modules/displayioBidAdapter_spec.js @@ -1,5 +1,5 @@ -import { expect } from 'chai' import {spec} from 'modules/displayioBidAdapter.js' +import {BANNER} from '/src/mediaTypes' describe('Displayio adapter', function () { const BIDDER = 'displayio' @@ -12,10 +12,7 @@ describe('Displayio adapter', function () { mediaTypes: { banner: { sizes: [[320, 480]] - }, - video: { - sizes: [[360, 640]] - }, + } }, params: { siteId: 1, @@ -128,53 +125,6 @@ describe('Displayio adapter', function () { }) }) - describe('_getPayload', function () { - const payload = spec._getPayload(bidRequests[0], bidderRequest) - it('should not be empty', function() { - expect(payload).to.not.be.empty - }) - - it('should have userSession', function() { - expect(payload.userSession).to.be.a('string') - }) - - it('should have data object', function() { - expect(payload.data).to.be.a('object') - }) - - it('should have complianceData object', function() { - expect(payload.data.complianceData).to.be.a('object') - }) - - it('should have device object', function() { - expect(payload.data.device).to.be.a('object') - }) - - it('should have omidpn', function() { - expect(payload.data.omidpn).to.be.a('string') - }) - - it('should have integration', function() { - expect(payload.data.integration).to.be.a('string') - }) - - it('should have bidId', function() { - expect(payload.data.id).to.not.be.empty - }) - - it('should have action getPlacement', function() { - expect(payload.data.action).to.be.equal('getPlacement') - }) - - it('should have app parameter', function() { - expect(payload.data.app).to.be.a('number') - }) - - it('should have placement parameter', function() { - expect(payload.data.placement).to.be.a('number') - }) - }) - describe('interpretResponse', function () { const response = { body: { @@ -199,6 +149,8 @@ describe('Displayio adapter', function () { data: { data: { id: 'id_001', + adUnitCode: 'test-div', + renderURL: 'testprebid.com/render.js', data: { ref: 'testprebid.com' } @@ -235,5 +187,17 @@ describe('Displayio adapter', function () { it('should have ad', function() { expect(ir.ad).to.be.a('string') }) + + it('should have mediaType', function() { + expect(ir.mediaType).to.be.equal(BANNER) + }) + + it('should have adUnitCode', function() { + expect(ir.adUnitCode).to.be.a('string') + }) + + it('should have renderURL', function() { + expect(ir.renderURL).to.be.a('string') + }) }) }) From 7e5548e4de15edb3b554fcd6c8a19a26c0bd36e2 Mon Sep 17 00:00:00 2001 From: Gena Date: Fri, 19 Aug 2022 21:46:50 +0200 Subject: [PATCH 13/25] VidCrunch LLC bidder (#8872) --- modules/adtelligentBidAdapter.js | 2 ++ test/spec/modules/adtelligentBidAdapter_spec.js | 1 + 2 files changed, 3 insertions(+) diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index 2ee5b0f72a3..3dad2e98bcf 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -23,6 +23,7 @@ const HOST_GETTERS = { janet: () => 'ghb.bidder.jmgads.com', pgam: () => 'ghb.pgamssp.com', ocm: () => 'ghb.cenarius.orangeclickmedia.com', + vidcrunchllc: () => 'ghb.platform.vidcrunch.com', } const getUri = function (bidderCode) { let bidderWithoutSuffix = bidderCode.split('_')[0]; @@ -43,6 +44,7 @@ export const spec = { { code: 'navelix', gvlid: 380 }, 'pgam', 'ocm', + { code: 'vidcrunchllc', gvlid: 1145 }, ], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index a9b9724da3a..d0ef69ccf08 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -20,6 +20,7 @@ const aliasEP = { janet: 'https://ghb.bidder.jmgads.com/v2/auction/', pgam: 'https://ghb.pgamssp.com/v2/auction/', ocm: 'https://ghb.cenarius.orangeclickmedia.com/v2/auction/', + vidcrunchllc: 'https://ghb.platform.vidcrunch.com/v2/auction/', }; const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; From 0b71a3364cf774a87f630157ab78c1a4f0670c18 Mon Sep 17 00:00:00 2001 From: matthieularere-msq <63732822+matthieularere-msq@users.noreply.github.com> Date: Mon, 22 Aug 2022 17:51:05 +0200 Subject: [PATCH 14/25] bidWatch Analytics Adapter : limit bandwidth usage + refactory (#8774) * bidWatch Analytics Adapter : code refactory * Update bidwatchAnalyticsAdapter_spec.js * Update bidwatchAnalyticsAdapter_spec.js --- modules/bidwatchAnalyticsAdapter.js | 171 ++++++++++++------ .../modules/bidwatchAnalyticsAdapter_spec.js | 40 ++-- 2 files changed, 140 insertions(+), 71 deletions(-) diff --git a/modules/bidwatchAnalyticsAdapter.js b/modules/bidwatchAnalyticsAdapter.js index ef63fee53d5..9983eb58289 100644 --- a/modules/bidwatchAnalyticsAdapter.js +++ b/modules/bidwatchAnalyticsAdapter.js @@ -12,6 +12,7 @@ const { BID_WON, BID_RESPONSE, BID_REQUESTED, + BID_TIMEOUT, } } = CONSTANTS; @@ -20,80 +21,125 @@ let allEvents = {} let auctionEnd = {} let initOptions = {} let endpoint = 'https://default' -let objectToSearchForBidderCode = ['bidderRequests', 'bidsReceived', 'noBids'] +let requestsAttributes = ['adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'params', 'userId', 'labelAny', 'bids']; function getAdapterNameForAlias(aliasName) { return adapterManager.aliasRegistry[aliasName] || aliasName; } -function cleanArgObject(arg, removead) { - if (typeof arg['bidderCode'] == 'string') { arg['originalBidder'] = getAdapterNameForAlias(arg['bidderCode']); } - if (typeof arg['creativeId'] == 'number') { - arg['creativeId'] = arg['creativeId'].toString(); - } - if (removead && typeof arg['ad'] != 'undefined') { - arg['ad'] = 'emptied'; - } - if (typeof arg['gdprConsent'] != 'undefined' && typeof arg['gdprConsent']['vendorData'] != 'undefined') { - arg['gdprConsent']['vendorData'] = 'emptied'; +function filterAttributes(arg, removead) { + let response = {}; + if (typeof arg == 'object') { + if (typeof arg['bidderCode'] == 'string') { + response['originalBidder'] = getAdapterNameForAlias(arg['bidderCode']); + } else if (typeof arg['bidder'] == 'string') { + response['originalBidder'] = getAdapterNameForAlias(arg['bidder']); + } + if (!removead && typeof arg['ad'] != 'undefined') { + response['ad'] = arg['ad']; + } + if (typeof arg['gdprConsent'] != 'undefined') { + response['gdprConsent'] = {}; + if (typeof arg['gdprConsent']['consentString'] != 'undefined') { response['gdprConsent']['consentString'] = arg['gdprConsent']['consentString']; } + } + requestsAttributes.forEach((attr) => { + if (typeof arg[attr] != 'undefined') { response[attr] = arg[attr]; } + }); + if (typeof response['creativeId'] == 'number') { response['creativeId'] = response['creativeId'].toString(); } } - return arg; + return response; } -function cleanArgs(arg, removead) { - Object.keys(arg).forEach(key => { - arg[key] = cleanArgObject(arg[key], removead); +function cleanAuctionEnd(args) { + let response = {}; + let filteredObj; + let objects = ['bidderRequests', 'bidsReceived', 'noBids']; + objects.forEach((attr) => { + if (Array.isArray(args[attr])) { + response[attr] = []; + args[attr].forEach((obj) => { + filteredObj = filterAttributes(obj, true); + if (typeof obj['bids'] == 'object') { + filteredObj['bids'] = []; + obj['bids'].forEach((bid) => { + filteredObj['bids'].push(filterAttributes(bid, true)); + }); + } + response[attr].push(filteredObj); + }); + } }); - return arg + return response; +} + +function cleanCreatives(args) { + return filterAttributes(args, false); } -function checkBidderCode(args, removead) { - if (typeof args == 'object') { - for (let i = 0; i < objectToSearchForBidderCode.length; i++) { - if (typeof args[objectToSearchForBidderCode[i]] == 'object') { args[objectToSearchForBidderCode[i]] = cleanArgs(args[objectToSearchForBidderCode[i]], removead) } +function enhanceMediaType(arg) { + saveEvents['bidRequested'].forEach((bidRequested) => { + if (bidRequested['auctionId'] == arg['auctionId'] && Array.isArray(bidRequested['bids'])) { + bidRequested['bids'].forEach((bid) => { + if (bid['transactionId'] == arg['transactionId'] && bid['bidId'] == arg['requestId']) { arg['mediaTypes'] = bid['mediaTypes']; } + }); } - } - if (typeof args['bidderCode'] == 'string') { args['originalBidder'] = getAdapterNameForAlias(args['bidderCode']); } else if (typeof args['bidder'] == 'string') { args['originalBidder'] = getAdapterNameForAlias(args['bidder']); } - if (typeof args['creativeId'] == 'number') { args['creativeId'] = args['creativeId'].toString(); } + }); + return arg; +} - return args +function addBidResponse(args) { + let eventType = BID_RESPONSE; + let argsCleaned = cleanCreatives(JSON.parse(JSON.stringify(args))); ; + if (allEvents[eventType] == undefined) { allEvents[eventType] = [] } + allEvents[eventType].push(argsCleaned); } -function addEvent(eventType, args) { - let argsCleaned; - if (eventType && args) { - if (allEvents[eventType] == undefined) { allEvents[eventType] = [] } - if (saveEvents[eventType] == undefined) { saveEvents[eventType] = [] } - argsCleaned = checkBidderCode(JSON.parse(JSON.stringify(args)), false); - allEvents[eventType].push(argsCleaned); - saveEvents[eventType].push(argsCleaned); - argsCleaned = checkBidderCode(JSON.parse(JSON.stringify(args)), true); - if (['auctionend', 'bidtimeout'].includes(eventType.toLowerCase())) { - if (auctionEnd[eventType] == undefined) { auctionEnd[eventType] = [] } - auctionEnd[eventType].push(argsCleaned); - } - } +function addBidRequested(args) { + let eventType = BID_REQUESTED; + let argsCleaned = filterAttributes(args, true); + if (saveEvents[eventType] == undefined) { saveEvents[eventType] = [] } + saveEvents[eventType].push(argsCleaned); +} + +function addTimeout(args) { + let eventType = BID_TIMEOUT; + if (saveEvents[eventType] == undefined) { saveEvents[eventType] = [] } + saveEvents[eventType].push(args); + let argsCleaned = []; + let argsDereferenced = JSON.parse(JSON.stringify(args)); + argsDereferenced.forEach((attr) => { + argsCleaned.push(filterAttributes(JSON.parse(JSON.stringify(attr)), false)); + }); + if (auctionEnd[eventType] == undefined) { auctionEnd[eventType] = [] } + auctionEnd[eventType].push(argsCleaned); +} + +function addAuctionEnd(args) { + let eventType = AUCTION_END; + if (saveEvents[eventType] == undefined) { saveEvents[eventType] = [] } + saveEvents[eventType].push(args); + let argsCleaned = cleanAuctionEnd(JSON.parse(JSON.stringify(args))); + if (auctionEnd[eventType] == undefined) { auctionEnd[eventType] = [] } + auctionEnd[eventType].push(argsCleaned); } function handleBidWon(args) { - args = cleanArgObject(JSON.parse(JSON.stringify(args)), true); + args = enhanceMediaType(filterAttributes(JSON.parse(JSON.stringify(args)), true)); let increment = args['cpm']; if (typeof saveEvents['auctionEnd'] == 'object') { - for (let i = 0; i < saveEvents['auctionEnd'].length; i++) { - let tmpAuction = saveEvents['auctionEnd'][i]; - if (tmpAuction['auctionId'] == args['auctionId'] && typeof tmpAuction['bidsReceived'] == 'object') { - for (let j = 0; j < tmpAuction['bidsReceived'].length; j++) { - let tmpBid = tmpAuction['bidsReceived'][j]; - if (tmpBid['transactionId'] == args['transactionId'] && tmpBid['adId'] != args['adId']) { - if (args['cpm'] < tmpBid['cpm']) { + saveEvents['auctionEnd'].forEach((auction) => { + if (auction['auctionId'] == args['auctionId'] && typeof auction['bidsReceived'] == 'object') { + auction['bidsReceived'].forEach((bid) => { + if (bid['transactionId'] == args['transactionId'] && bid['adId'] != args['adId']) { + if (args['cpm'] < bid['cpm']) { increment = 0; - } else if (increment > args['cpm'] - tmpBid['cpm']) { - increment = args['cpm'] - tmpBid['cpm']; + } else if (increment > args['cpm'] - bid['cpm']) { + increment = args['cpm'] - bid['cpm']; } } - } + }); } - } + }); } args['cpmIncrement'] = increment; if (typeof saveEvents.bidRequested == 'object' && saveEvents.bidRequested.length > 0 && saveEvents.bidRequested[0].gdprConsent) { args.gdpr = saveEvents.bidRequested[0].gdprConsent; } @@ -101,12 +147,16 @@ function handleBidWon(args) { } function handleAuctionEnd() { - ajax(endpoint + '.bidwatch.io/analytics/auctions', null, JSON.stringify(auctionEnd), {method: 'POST', withCredentials: true}); - auctionEnd = {} - if (typeof allEvents['bidResponse'] != 'undefined') { - for (let i = 0; i < allEvents['bidResponse'].length; i++) { ajax(endpoint + '.bidwatch.io/analytics/creatives', null, JSON.stringify(allEvents['bidResponse'][i]), {method: 'POST', withCredentials: true}); } - } - allEvents = {} + ajax(endpoint + '.bidwatch.io/analytics/auctions', function (data) { + let list = JSON.parse(data); + if (Array.isArray(list) && typeof allEvents['bidResponse'] != 'undefined') { + allEvents['bidResponse'].forEach((bidResponse) => { + if (list.includes(bidResponse['originalBidder'] + '_' + bidResponse['creativeId'])) { ajax(endpoint + '.bidwatch.io/analytics/creatives', null, JSON.stringify(bidResponse), {method: 'POST', withCredentials: true}); } + }); + } + allEvents = {}; + }, JSON.stringify(auctionEnd), {method: 'POST', withCredentials: true}); + auctionEnd = {}; } let bidwatchAnalytics = Object.assign(adapter({url, analyticsType}), { @@ -116,17 +166,20 @@ let bidwatchAnalytics = Object.assign(adapter({url, analyticsType}), { }) { switch (eventType) { case AUCTION_END: - addEvent(eventType, args); + addAuctionEnd(args); handleAuctionEnd(); break; case BID_WON: handleBidWon(args); break; case BID_RESPONSE: - addEvent(eventType, args); + addBidResponse(args); break; case BID_REQUESTED: - addEvent(eventType, args); + addBidRequested(args); + break; + case BID_TIMEOUT: + addTimeout(args); break; } }}); diff --git a/test/spec/modules/bidwatchAnalyticsAdapter_spec.js b/test/spec/modules/bidwatchAnalyticsAdapter_spec.js index f827f068bb3..dddb29dbfd9 100644 --- a/test/spec/modules/bidwatchAnalyticsAdapter_spec.js +++ b/test/spec/modules/bidwatchAnalyticsAdapter_spec.js @@ -260,15 +260,15 @@ describe('BidWatch Analytics', function () { describe('main test flow', function () { beforeEach(function () { sinon.stub(events, 'getEvents').returns([]); + sinon.spy(bidwatchAnalytics, 'track'); }); - afterEach(function () { events.getEvents.restore(); + bidwatchAnalytics.disableAnalytics(); + bidwatchAnalytics.track.restore(); }); - it('should catch events of interest', function () { - sinon.spy(bidwatchAnalytics, 'track'); - + it('test auctionEnd', function () { adapterManager.registerAnalyticsAdapter({ code: 'bidwatch', adapter: bidwatchAnalytics @@ -280,6 +280,9 @@ describe('BidWatch Analytics', function () { domain: 'test' } }); + + events.emit(constants.EVENTS.BID_REQUESTED, auctionEnd['bidderRequests'][0]); + events.emit(constants.EVENTS.BID_RESPONSE, auctionEnd['bidsReceived'][0]); events.emit(constants.EVENTS.BID_TIMEOUT, bidTimeout); events.emit(constants.EVENTS.AUCTION_END, auctionEnd); expect(server.requests.length).to.equal(1); @@ -287,18 +290,31 @@ describe('BidWatch Analytics', function () { expect(message).to.have.property('auctionEnd').exist; expect(message.auctionEnd).to.have.lengthOf(1); expect(message.auctionEnd[0]).to.have.property('bidsReceived').and.to.have.lengthOf(1); - expect(message.auctionEnd[0].bidsReceived[0]).to.have.property('ad'); - expect(message.auctionEnd[0].bidsReceived[0].ad).to.equal('emptied'); + expect(message.auctionEnd[0].bidsReceived[0]).not.to.have.property('ad'); expect(message.auctionEnd[0]).to.have.property('bidderRequests').and.to.have.lengthOf(1); expect(message.auctionEnd[0].bidderRequests[0]).to.have.property('gdprConsent'); - expect(message.auctionEnd[0].bidderRequests[0].gdprConsent).to.have.property('vendorData'); - expect(message.auctionEnd[0].bidderRequests[0].gdprConsent.vendorData).to.equal('emptied'); + expect(message.auctionEnd[0].bidderRequests[0].gdprConsent).not.to.have.property('vendorData'); + sinon.assert.callCount(bidwatchAnalytics.track, 4); + }); + + it('test bidWon', function() { + adapterManager.registerAnalyticsAdapter({ + code: 'bidwatch', + adapter: bidwatchAnalytics + }); + + adapterManager.enableAnalytics({ + provider: 'bidwatch', + options: { + domain: 'test' + } + }); events.emit(constants.EVENTS.BID_WON, bidWon); - expect(server.requests.length).to.equal(2); - message = JSON.parse(server.requests[1].requestBody); - expect(message).to.have.property('ad').and.to.equal('emptied'); + expect(server.requests.length).to.equal(1); + let message = JSON.parse(server.requests[0].requestBody); + expect(message).not.to.have.property('ad'); expect(message).to.have.property('cpmIncrement').and.to.equal(27.4276); - sinon.assert.callCount(bidwatchAnalytics.track, 3); + sinon.assert.callCount(bidwatchAnalytics.track, 1); }); }); }); From a61f4bfdc31c6ade12c19489b6edbe2126711277 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Aug 2022 13:37:11 -0400 Subject: [PATCH 15/25] Bump tibdex/github-app-token from 1.3.0 to 1.6 (#8878) Bumps [tibdex/github-app-token](https://github.com/tibdex/github-app-token) from 1.3.0 to 1.6. - [Release notes](https://github.com/tibdex/github-app-token/releases) - [Commits](https://github.com/tibdex/github-app-token/compare/36464acb844fc53b9b8b2401da68844f6b05ebb0...f717b5ecd4534d3c4df4ce9b5c1c2214f0f7cd06) --- updated-dependencies: - dependency-name: tibdex/github-app-token dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue_tracker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue_tracker.yml b/.github/workflows/issue_tracker.yml index fa33ffe5c53..05d08b2b0d7 100644 --- a/.github/workflows/issue_tracker.yml +++ b/.github/workflows/issue_tracker.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: tibdex/github-app-token@36464acb844fc53b9b8b2401da68844f6b05ebb0 + uses: tibdex/github-app-token@f717b5ecd4534d3c4df4ce9b5c1c2214f0f7cd06 with: app_id: ${{ secrets.ISSUE_APP_ID }} private_key: ${{ secrets.ISSUE_APP_PEM }} From 5be582f2e35650e0bf4dbe212bde9e936edfe75c Mon Sep 17 00:00:00 2001 From: Denis Logachov Date: Mon, 22 Aug 2022 20:44:12 +0300 Subject: [PATCH 16/25] Adkernel Bid Adapter: add impression-level FPD support (#8880) --- modules/adkernelBidAdapter.js | 40 ++++++++++++++------ test/spec/modules/adkernelBidAdapter_spec.js | 13 ++++++- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 78f00784462..ff7d3be6ebf 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -14,11 +14,12 @@ import { isPlainObject, isStr, mergeDeep, - parseGPTSingleSizeArrayToRtbSize + parseGPTSingleSizeArrayToRtbSize, + getDefinedParams } from '../src/utils.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {find, includes} from '../src/polyfill.js'; +import {find} from '../src/polyfill.js'; import {config} from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; @@ -28,10 +29,11 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; * * Please contact prebid@adkernel.com and we'll add your adapter as an alias. */ - -const VIDEO_TARGETING = Object.freeze(['mimes', 'minduration', 'maxduration', 'protocols', - 'startdelay', 'linearity', 'boxingallowed', 'playbackmethod', 'delivery', - 'pos', 'api', 'ext']); +const VIDEO_PARAMS = ['pos', 'context', 'placement', 'api', 'mimes', 'protocols', 'playbackmethod', 'minduration', 'maxduration', + 'startdelay', 'linearity', 'skip', 'skipmin', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackend', 'boxingallowed']; +const VIDEO_FPD = ['battr', 'pos']; +const NATIVE_FPD = ['battr', 'api']; +const BANNER_FPD = ['btype', 'battr', 'pos', 'api']; const VERSION = '1.6'; const SYNC_IFRAME = 1; const SYNC_IMAGE = 2; @@ -275,18 +277,18 @@ function buildImp(bidRequest, secure) { format: sizes.map(wh => parseGPTSingleSizeArrayToRtbSize(wh)), topframe: 0 }; + populateImpFpd(imp.banner, bidRequest, BANNER_FPD); mediaType = BANNER; } else if (deepAccess(bidRequest, 'mediaTypes.video')) { let video = deepAccess(bidRequest, 'mediaTypes.video'); - imp.video = {}; + imp.video = getDefinedParams(video, VIDEO_PARAMS); + populateImpFpd(imp.video, bidRequest, VIDEO_FPD); if (video.playerSize) { sizes = video.playerSize[0]; imp.video = Object.assign(imp.video, parseGPTSingleSizeArrayToRtbSize(sizes) || {}); - } - if (bidRequest.params.video) { - Object.keys(bidRequest.params.video) - .filter(key => includes(VIDEO_TARGETING, key)) - .forEach(key => imp.video[key] = bidRequest.params.video[key]); + } else if (video.w && video.h) { + imp.video.w = video.w; + imp.video.h = video.h; } mediaType = VIDEO; } else if (deepAccess(bidRequest, 'mediaTypes.native')) { @@ -295,6 +297,7 @@ function buildImp(bidRequest, secure) { ver: '1.1', request: JSON.stringify(nativeRequest) }; + populateImpFpd(imp.native, bidRequest, NATIVE_FPD); mediaType = NATIVE; } else { throw new Error('Unsupported bid received'); @@ -338,6 +341,19 @@ function buildNativeRequest(nativeReq) { return request; } +/** + * Populate impression-level FPD from bid request + * @param target {Object} + * @param bidRequest {BidRequest} + * @param props {String[]} + */ +function populateImpFpd(target, bidRequest, props) { + if (bidRequest.ortb2Imp === undefined) { + return; + } + Object.assign(target, getDefinedParams(bidRequest.ortb2Imp, props)); +} + /** * Builds image asset request */ diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 4b0eebdf519..45498d2734a 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -17,6 +17,9 @@ describe('Adkernel adapter', function () { banner: { sizes: [[300, 250], [300, 200]] } + }, + ortb2Imp: { + battr: [6, 7, 9] } }, bid2_zone2 = { bidder: 'adkernel', @@ -95,12 +98,12 @@ describe('Adkernel adapter', function () { params: { zoneId: 1, host: 'rtb.adkernel.com', - video: {api: [1, 2]} }, mediaTypes: { video: { context: 'instream', - playerSize: [[640, 480]] + playerSize: [[640, 480]], + api: [1, 2] } }, adUnitCode: 'ad-unit-1' @@ -293,6 +296,7 @@ describe('Adkernel adapter', function () { describe('banner request building', function () { let bidRequest, bidRequests, _; + before(function () { [_, bidRequests] = buildRequest([bid1_zone1]); bidRequest = bidRequests[0]; @@ -337,6 +341,11 @@ describe('Adkernel adapter', function () { expect(bidRequest.device).to.have.property('dnt', 1); }); + it('should copy FPD to imp.banner', function() { + expect(bidRequest.imp[0].banner).to.have.property('battr'); + expect(bidRequest.imp[0].banner.battr).to.be.eql([6, 7, 9]); + }); + it('shouldn\'t contain gdpr nor ccpa information for default request', function () { let [_, bidRequests] = buildRequest([bid1_zone1]); expect(bidRequests[0]).to.not.have.property('regs'); From 47c8ead2fe1d51282000dfe1df55b1dfa92aa53b Mon Sep 17 00:00:00 2001 From: Jason Piros Date: Mon, 22 Aug 2022 15:21:33 -0700 Subject: [PATCH 17/25] consumableBidAdapter: remove impressionUrl (#8883) --- modules/consumableBidAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/consumableBidAdapter.js b/modules/consumableBidAdapter.js index 6d502b24e81..87cb6052630 100644 --- a/modules/consumableBidAdapter.js +++ b/modules/consumableBidAdapter.js @@ -1,4 +1,4 @@ -import { logWarn, createTrackPixelHtml, deepAccess, isArray, deepSetValue } from '../src/utils.js'; +import { logWarn, deepAccess, isArray, deepSetValue } from '../src/utils.js'; import {config} from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; @@ -255,7 +255,7 @@ function getSize(sizes) { function retrieveAd(decision, unitId, unitName) { let ad; if (decision.contents && decision.contents[0]) { - ad = decision.contents[0].body + createTrackPixelHtml(decision.impressionUrl); + ad = decision.contents[0].body; } if (decision.vastXml) { ad = decision.vastXml; From 73c13cde704ef396a152ed9dcb8596779473107d Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Tue, 23 Aug 2022 09:03:58 -0400 Subject: [PATCH 18/25] Prebid Core: Add ttl buffer to videoCache.js (#8861) * Update videoCache.js * Update videoCache.js * Update videoCache_spec.js * Update videoCache_spec.js * master into ttl-buffer (#8869) * Update Sonobi adapter with GVLID (#8860) * dgkeyword RTD provider: fix tests causing ID5 test failures (#8862) Co-authored-by: Mike Miller Co-authored-by: Demetrio Girardi * Revert "master into ttl-buffer (#8869)" (#8879) This reverts commit b253980d38b6c801bc359066258b5988ce938865. * Name the constant Co-authored-by: Mike Miller Co-authored-by: Demetrio Girardi --- src/videoCache.js | 10 ++++++++-- test/spec/videoCache_spec.js | 14 +++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/videoCache.js b/src/videoCache.js index 219bca34726..f69a20f0139 100644 --- a/src/videoCache.js +++ b/src/videoCache.js @@ -13,6 +13,12 @@ import { ajax } from './ajax.js'; import { config } from './config.js'; import {auctionManager} from './auctionManager.js'; +/** + * Might be useful to be configurable in the future + * Depending on publisher needs + */ +const ttlBufferInSeconds = 15; + /** * @typedef {object} CacheableUrlBid * @property {string} vastUrl A URL which loads some valid VAST XML. @@ -63,11 +69,11 @@ function wrapURI(uri, impUrl) { function toStorageRequest(bid, {index = auctionManager.index} = {}) { const vastValue = bid.vastXml ? bid.vastXml : wrapURI(bid.vastUrl, bid.vastImpUrl); const auction = index.getAuction(bid); - + const ttlWithBuffer = Number(bid.ttl) + ttlBufferInSeconds; let payload = { type: 'xml', value: vastValue, - ttlseconds: Number(bid.ttl) + ttlseconds: ttlWithBuffer }; if (config.getConfig('cache.vasttrack')) { diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index fdb4103baed..5885dfb7cdf 100644 --- a/test/spec/videoCache_spec.js +++ b/test/spec/videoCache_spec.js @@ -155,12 +155,12 @@ describe('The video cache', function () { puts: [{ type: 'xml', value: vastXml1, - ttlseconds: 25, + ttlseconds: 40, key: customKey1 }, { type: 'xml', value: vastXml2, - ttlseconds: 25, + ttlseconds: 40, key: customKey2 }] }; @@ -205,7 +205,7 @@ describe('The video cache', function () { puts: [{ type: 'xml', value: vastXml1, - ttlseconds: 25, + ttlseconds: 40, key: customKey1, bidid: '12345abc', aid: '1234-56789-abcde', @@ -213,7 +213,7 @@ describe('The video cache', function () { }, { type: 'xml', value: vastXml2, - ttlseconds: 25, + ttlseconds: 40, key: customKey2, bidid: 'cba54321', aid: '1234-56789-abcde', @@ -276,7 +276,7 @@ describe('The video cache', function () { puts: [{ type: 'xml', value: vastXml1, - ttlseconds: 25, + ttlseconds: 40, key: customKey1, bidid: '12345abc', bidder: 'appnexus', @@ -285,7 +285,7 @@ describe('The video cache', function () { }, { type: 'xml', value: vastXml2, - ttlseconds: 25, + ttlseconds: 40, key: customKey2, bidid: 'cba54321', bidder: 'rubicon', @@ -309,7 +309,7 @@ describe('The video cache', function () { puts: [{ type: 'xml', value: expectedValue, - ttlseconds: 25 + ttlseconds: 40 }], }); } From 8d88902ff6f1479ac991d78d611e482f49c9c81c Mon Sep 17 00:00:00 2001 From: pm-azhar-mulla <75726247+pm-azhar-mulla@users.noreply.github.com> Date: Tue, 23 Aug 2022 21:06:16 +0530 Subject: [PATCH 19/25] Added support to log extra bids from same bidder (#8886) Co-authored-by: pm-azhar-mulla --- modules/pubmaticAnalyticsAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index 38bcc4521be..c8dc7cef15d 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -382,7 +382,7 @@ function bidResponseHandler(args) { return; } - if (bid.bidder && args.bidderCode && bid.bidder !== args.bidderCode) { + if ((bid.bidder && args.bidderCode && bid.bidder !== args.bidderCode) || (bid.bidder === args.bidderCode && bid.status === SUCCESS)) { bid = copyRequiredBidDetails(args); cache.auctions[args.auctionId].adUnitCodes[args.adUnitCode].bids[args.requestId].push(bid); } From 07232521ed1d8185adc99a25a4eb4c266281d756 Mon Sep 17 00:00:00 2001 From: Ignat Khaylov Date: Wed, 24 Aug 2022 11:23:30 +0300 Subject: [PATCH 20/25] BetweenBidAdapter: default value for the cur parameter (#8870) * BetweenBidAdapter: default value for the cur parameter * fix linting Co-authored-by: Ignat Khaylov Co-authored-by: Chris Huie --- modules/betweenBidAdapter.js | 6 +++--- test/spec/modules/betweenBidAdapter_spec.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index 2ca829b796b..ea28420481d 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -59,9 +59,9 @@ export const spec = { if (i.params.itu !== undefined) { params.itu = i.params.itu; } - if (i.params.cur !== undefined) { - params.cur = i.params.cur; - } + + params.cur = i.params.cur || 'USD'; + if (i.params.subid !== undefined) { params.subid = i.params.subid; } diff --git a/test/spec/modules/betweenBidAdapter_spec.js b/test/spec/modules/betweenBidAdapter_spec.js index 3baa92e35d5..a4b89ab1b65 100644 --- a/test/spec/modules/betweenBidAdapter_spec.js +++ b/test/spec/modules/betweenBidAdapter_spec.js @@ -85,6 +85,23 @@ describe('betweenBidAdapterTests', function () { expect(req_data.cur).to.equal('THX'); }); + it('validate default cur USD', function() { + let bidRequestData = [{ + bidId: 'bid1234', + bidder: 'between', + params: { + w: 240, + h: 400, + s: 1112 + }, + sizes: [[240, 400]] + }]; + + let request = spec.buildRequests(bidRequestData); + let req_data = JSON.parse(request.data)[0].data; + + expect(req_data.cur).to.equal('USD'); + }); it('validate subid param', function() { let bidRequestData = [{ bidId: 'bid1234', From 8a82e0935d7797513537e3f081a6f7afa0de0bdb Mon Sep 17 00:00:00 2001 From: BaronJHYu <254878848@qq.com> Date: Wed, 24 Aug 2022 22:17:01 +0800 Subject: [PATCH 21/25] Mediago Bid Adapter: initial adapter release (#8856) * Mediago Bid Adapter:new adapter * remove console * change spec file to fix CircleCI * change spec file to fix CircleCI * change spec file * Update mediagoBidAdapter.js * Update mediagoBidAdapter.js * rerun CurcleCi Co-authored-by: BaronYu --- modules/mediagoBidAdapter.js | 462 ++++++++++++++++++++ modules/mediagoBidAdapter.md | 34 ++ test/spec/modules/mediagoBidAdapter_spec.js | 97 ++++ 3 files changed, 593 insertions(+) create mode 100644 modules/mediagoBidAdapter.js create mode 100644 modules/mediagoBidAdapter.md create mode 100644 test/spec/modules/mediagoBidAdapter_spec.js diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js new file mode 100644 index 00000000000..28ca4e0bc50 --- /dev/null +++ b/modules/mediagoBidAdapter.js @@ -0,0 +1,462 @@ +/** + * gulp serve --modules=mediagoBidAdapter,pubCommonId --nolint --notest + */ + +import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +// import { config } from '../src/config.js'; +// import { isPubcidEnabled } from './pubCommonId.js'; + +const BIDDER_CODE = 'mediago'; +// const PROTOCOL = window.document.location.protocol; +const ENDPOINT_URL = + // ((PROTOCOL === 'https:') ? 'https' : 'http') + + 'https://rtb-us.mediago.io/api/bid?tn='; +const TIME_TO_LIVE = 500; +// const ENDPOINT_URL = '/api/bid?tn='; +const storage = getStorageManager(); +let globals = {}; +let itemMaps = {}; + +/** + * 获取随机id + * @param {number} a random number from 0 to 15 + * @return {string} random number or random string + */ +// function getRandomId( +// a // placeholder +// ) { +// // if the placeholder was passed, return +// // a random number from 0 to 15 +// return a +// ? ( +// a ^ // unless b is 8, +// ((Math.random() * // in which case +// 16) >> // a random number from +// (a / 4)) +// ) // 8 to 11 +// .toString(16) // in hexadecimal +// : ( // or otherwise a concatenated string: +// [1e7] + // 10000000 + +// 1e3 + // -1000 + +// 4e3 + // -4000 + +// 8e3 + // -80000000 + +// 1e11 +// ) // -100000000000, +// .replace( +// // replacing +// /[018]/g, // zeroes, ones, and eights with +// getRandomId // random hex digits +// ); +// } + +/* ----- mguid:start ------ */ +const COOKIE_KEY_MGUID = '__mguid_'; + +/** + * 获取用户id + * @return {string} + */ +const getUserID = () => { + const i = storage.getCookie(COOKIE_KEY_MGUID); + + if (i === null) { + const uuid = utils.generateUUID(); + storage.setCookie(COOKIE_KEY_MGUID, uuid); + return uuid; + } + return i; +}; + +/* ----- mguid:end ------ */ + +/** + * 获取一个对象的某个值,如果没有则返回空字符串 + * @param {Object} obj 对象 + * @param {...string} keys 键名 + * @return {any} + */ +function getProperty(obj, ...keys) { + let o = obj; + + for (let key of keys) { + // console.log(key, o); + if (o && o[key]) { + o = o[key]; + } else { + return ''; + } + } + return o; +} + +/** + * 是不是移动设备或者平板 + * @return {boolean} + */ +function isMobileAndTablet() { + let check = false; + (function (a) { + let reg1 = new RegExp( + [ + '(android|bbd+|meego)', + '.+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)', + '|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone', + '|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up.(browser|link)|vodafone|wap', + '|windows ce|xda|xiino|android|ipad|playbook|silk', + ].join(''), + 'i' + ); + let reg2 = new RegExp( + [ + '1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)', + '|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )', + '|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55/|capi|ccwa|cdm-|cell', + '|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)', + '|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene', + '|gf-5|g-mo|go(.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c', + '|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|/)|ibro|idea|ig01|ikom', + '|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |/)|klon|kpt |kwc-|kyo(c|k)', + '|le(no|xi)|lg( g|/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50/|ma(te|ui|xo)|mc(01|21|ca)', + '|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]', + '|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)', + '|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio', + '|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55/|sa(ge|ma|mm|ms', + '|ny|va)|sc(01|h-|oo|p-)|sdk/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al', + '|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)', + '|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(.b|g1|si)|utst|', + 'v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)', + '|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-', + '|your|zeto|zte-', + ].join(''), + 'i' + ); + if (reg1.test(a) || reg2.test(a.substr(0, 4))) { + check = true; + } + })(navigator.userAgent || navigator.vendor || window.opera); + return check; +} + +/** + * 获取底价 + * @param {*} bid + * @param {*} mediaType + * @param {*} sizes + * @returns + */ +// function getBidFloor(bid, mediaType, sizes) { +// var floor; +// var size = sizes.length === 1 ? sizes[0] : "*"; +// if (typeof bid.getFloor === "function") { +// const floorInfo = bid.getFloor({ currency: "USD", mediaType, size }); +// if ( +// typeof floorInfo === "object" && +// floorInfo.currency === "USD" && +// !isNaN(parseFloat(floorInfo.floor)) +// ) { +// floor = parseFloat(floorInfo.floor); +// } +// } +// return floor; +// } +function getBidFloor(bid) { + if (!utils.isFn(bid.getFloor)) { + return utils.deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0; + } +} + +/** + * 将尺寸转为RTB识别的尺寸 + * + * @param {Array|Object} requestSizes 配置尺寸 + * @return {Object} + */ +function transformSizes(requestSizes) { + let sizes = []; + let sizeObj = {}; + + if ( + utils.isArray(requestSizes) && + requestSizes.length === 2 && + !utils.isArray(requestSizes[0]) + ) { + sizeObj.width = parseInt(requestSizes[0], 10); + sizeObj.height = parseInt(requestSizes[1], 10); + sizes.push(sizeObj); + } else if (typeof requestSizes === 'object') { + for (let i = 0; i < requestSizes.length; i++) { + let size = requestSizes[i]; + sizeObj = {}; + sizeObj.width = parseInt(size[0], 10); + sizeObj.height = parseInt(size[1], 10); + sizes.push(sizeObj); + } + } + + return sizes; +} + +// 支持的广告尺寸 +const mediagoAdSize = [ + { w: 300, h: 250 }, + { w: 300, h: 600 }, + { w: 728, h: 90 }, + { w: 970, h: 250 }, + { w: 320, h: 50 }, + { w: 160, h: 600 }, + { w: 320, h: 180 }, + { w: 320, h: 100 }, + { w: 336, h: 280 }, +]; + +/** + * 获取广告位配置 + * @param {Array} validBidRequests an an array of bids + * @param {Object} bidderRequest The master bidRequest object + * @return {Object} + */ +function getItems(validBidRequests, bidderRequest) { + let items = []; + items = validBidRequests.map((req, i) => { + let ret = {}; + let mediaTypes = getProperty(req, 'mediaTypes'); + + let sizes = transformSizes(getProperty(req, 'sizes')); + let matchSize; + + // 确认尺寸是否符合我们要求 + for (let size of sizes) { + matchSize = mediagoAdSize.find( + (item) => size.width === item.w && size.height === item.h + ); + if (matchSize) { + break; + } + } + if (!matchSize) { + return {}; + } + + const bidFloor = getBidFloor(req); + // const gpid = + // utils.deepAccess(req, 'ortb2Imp.ext.gpid') || + // utils.deepAccess(req, 'ortb2Imp.ext.data.pbadslot') || + // utils.deepAccess(req, 'params.placementId', 0); + // console.log("wjh getItems:", req, bidFloor, gpid); + + // if (mediaTypes.native) {} + // banner广告类型 + if (mediaTypes.banner) { + let id = '' + (i + 1); + ret = { + id: id, + bidfloor: bidFloor, + banner: { + h: matchSize.h, + w: matchSize.w, + pos: 1, + }, + ext: { + // gpid: gpid, // 加入后无法返回广告 + }, + }; + itemMaps[id] = { + req, + ret, + }; + } + + return ret; + }); + return items; +} + +/** + * 获取rtb请求参数 + * + * @param {Array} validBidRequests an an array of bids + * @param {Object} bidderRequest The master bidRequest object + * @return {Object} + */ +function getParam(validBidRequests, bidderRequest) { + const pubcid = utils.deepAccess(validBidRequests[0], 'crumbs.pubcid'); + // console.log('wjh getParam', validBidRequests, bidderRequest); + let isMobile = isMobileAndTablet() ? 1 : 0; + let isTest = 0; + let auctionId = getProperty(bidderRequest, 'auctionId'); + let items = getItems(validBidRequests, bidderRequest); + + const domain = document.domain; + const location = utils.deepAccess(bidderRequest, 'refererInfo.referer'); + + const timeout = bidderRequest.timeout || 2000; + + if (items && items.length) { + let c = { + id: 'mgprebidjs_' + auctionId, + test: +isTest, + at: 1, + cur: ['USD'], + device: { + connectiontype: 0, + // ip: '64.188.178.115', + js: 1, + // language: "en", + // os: "Microsoft Windows", + // ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19043", + os: navigator.platform || '', + ua: navigator.userAgent, + language: /en/.test(navigator.language) ? 'en' : navigator.language, + }, + ext: {}, + user: { + buyeruid: getUserID(), + id: pubcid, + }, + site: { + name: domain, + domain: domain, + page: location, + ref: location, + mobile: isMobile, + cat: [], // todo + publisher: { + // todo + id: domain, + name: domain, + }, + }, + imp: items, + tmax: timeout, + }; + return c; + } else { + return null; + } +} + +export const spec = { + code: BIDDER_CODE, + // aliases: ['ex'], // short code + /** + * 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) { + // console.log('mediago', { + // bid + // }); + if (bid.params.token) { + globals['token'] = bid.params.token; + } + return !!bid.params.token; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {Array} validBidRequests an an array of bids + * @param {Object} bidderRequest The master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + let payload = getParam(validBidRequests, bidderRequest); + + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: ENDPOINT_URL + globals['token'], + data: payloadString, + }; + }, + + /** + * 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) { + const bids = getProperty(serverResponse, 'body', 'seatbid', 0, 'bid'); + const cur = getProperty(serverResponse, 'body', 'cur'); + + const bidResponses = []; + for (let bid of bids) { + let impid = getProperty(bid, 'impid'); + if (itemMaps[impid]) { + let bidId = getProperty(itemMaps[impid], 'req', 'bidId'); + const bidResponse = { + requestId: bidId, + cpm: getProperty(bid, 'price'), + width: getProperty(bid, 'w'), + height: getProperty(bid, 'h'), + creativeId: getProperty(bid, 'crid'), + dealId: '', + currency: cur, + netRevenue: true, + ttl: TIME_TO_LIVE, + // referrer: REFERER, + ad: getProperty(bid, 'adm'), + nurl: getProperty(bid, 'nurl'), + // adserverTargeting: { + // granularityMultiplier: 0.1, + // priceGranularity: "pbHg", + // pbMg: "0.01", + // }, + // pbMg: "0.01", + // granularityMultiplier: 0.1, + // priceGranularity: "pbHg", + }; + bidResponses.push(bidResponse); + } + } + + return bidResponses; + }, + + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ + // onTimeout: function (data) { + // // console.log('onTimeout', data); + // // Bidder specifc code + // }, + + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ + onBidWon: function (bid) { + // console.log('onBidWon: ', bid, config.getConfig('priceGranularity')); + // Bidder specific code + if (bid['nurl']) { + utils.triggerPixel(bid['nurl']); + } + }, + + /** + * Register bidder specific code, which will execute when the adserver targeting has been set for a bid from this bidder + * @param {Bid} The bid of which the targeting has been set + */ +// onSetTargeting: function (bid) { +// // console.log('onSetTargeting', bid); +// // Bidder specific code +// }, +}; +registerBidder(spec); diff --git a/modules/mediagoBidAdapter.md b/modules/mediagoBidAdapter.md new file mode 100644 index 00000000000..464ea59f11c --- /dev/null +++ b/modules/mediagoBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: MediaGo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: fangsimin@baidu.com +``` + +# Description + +Module that connects to MediaGo's demand sources + +# Test Parameters + +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: "mediago", + params: { + token: '' // required, send email to ext_mediago_am@baidu.com to get the corresponding token + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/mediagoBidAdapter_spec.js b/test/spec/modules/mediagoBidAdapter_spec.js new file mode 100644 index 00000000000..e77af544429 --- /dev/null +++ b/test/spec/modules/mediagoBidAdapter_spec.js @@ -0,0 +1,97 @@ +import { expect } from 'chai'; +import { spec } from 'modules/mediagoBidAdapter.js'; + +describe('mediago:BidAdapterTests', function () { + let bidRequestData = { + bidderCode: 'mediago', + auctionId: '7fae02a9-0195-472f-ba94-708d3bc2c0d9', + bidderRequestId: '4fec04e87ad785', + bids: [ + { + bidder: 'mediago', + params: { + token: '85a6b01e41ac36d49744fad726e3655d', + bidfloor: 0.01, + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + adUnitCode: 'regular_iframe', + transactionId: '7b26fdae-96e6-4c35-a18b-218dda11397d', + sizes: [[300, 250]], + bidId: '54d73f19c9d47a', // todo + bidderRequestId: '4fec04e87ad785', // todo + auctionId: '883a346a-6d62-4adb-a600-0f3a869061d1', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + }, + ], + }; + let request = []; + + it('mediago:validate_pub_params', function () { + expect( + spec.isBidRequestValid({ + bidder: 'mediago', + params: { + token: ['85a6b01e41ac36d49744fad726e3655d'], + }, + }) + ).to.equal(true); + }); + + it('mediago:validate_generated_params', function () { + request = spec.buildRequests(bidRequestData.bids, bidRequestData); + let req_data = JSON.parse(request.data); + expect(req_data.imp).to.have.lengthOf(1); + }); + + it('mediago:validate_response_params', function () { + let adm = ""; + let temp = '%3Cscr'; + temp += 'ipt%3E'; + temp += '!function()%7B%22use%20strict%22%3Bfunction%20f(t)%7Breturn(f%3D%22function%22%3D%3Dtypeof%20Symbol%26%26%22symbol%22%3D%3Dtypeof%20Symbol.iterator%3Ffunction(t)%7Breturn%20typeof%20t%7D%3Afunction(t)%7Breturn%20t%26%26%22function%22%3D%3Dtypeof%20Symbol%26%26t.constructor%3D%3D%3DSymbol%26%26t!%3D%3DSymbol.prototype%3F%22symbol%22%3Atypeof%20t%7D)(t)%7Dfunction%20l(t)%7Bvar%20e%3D0%3Carguments.length%26%26void%200!%3D%3Dt%3Ft%3A%7B%7D%3Btry%7Be.random_t%3D(new%20Date).getTime()%2Cg(function(t)%7Bvar%20e%3D1%3Carguments.length%26%26void%200!%3D%3Darguments%5B1%5D%3Farguments%5B1%5D%3A%22%22%3Bif(%22object%22!%3D%3Df(t))return%20e%3Bvar%20n%3Dfunction(t)%7Bfor(var%20e%2Cn%3D%5B%5D%2Co%3D0%2Ci%3DObject.keys(t)%3Bo%3Ci.length%3Bo%2B%2B)e%3Di%5Bo%5D%2Cn.push(%22%22.concat(e%2C%22%3D%22).concat(t%5Be%5D))%3Breturn%20n%7D(t).join(%22%26%22)%2Co%3De.indexOf(%22%23%22)%2Ci%3De%2Ct%3D%22%22%3Breturn-1!%3D%3Do%26%26(i%3De.slice(0%2Co)%2Ct%3De.slice(o))%2Cn%26%26(i%26%26-1!%3D%3Di.indexOf(%22%3F%22)%3Fi%2B%3D%22%26%22%2Bn%3Ai%2B%3D%22%3F%22%2Bn)%2Ci%2Bt%7D(e%2C%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Flog%2Ftrack%22))%7Dcatch(t)%7B%7D%7Dfunction%20g(t%2Ce%2Cn)%7B(t%3Dt%3Ft.split(%22%3B%3B%3B%22)%3A%5B%5D).map(function(t)%7Btry%7B0%3C%3Dt.indexOf(%22%2Fapi%2Fbidder%2Ftrack%22)%26%26n%26%26(t%2B%3D%22%26inIframe%3D%22.concat(!(!self.frameElement%7C%7C%22IFRAME%22!%3Dself.frameElement.tagName)%7C%7Cwindow.frames.length!%3Dparent.frames.length%7C%7Cself!%3Dtop)%2Ct%2B%3D%22%26pos_x%3D%22.concat(n.left%2C%22%26pos_y%3D%22).concat(n.top%2C%22%26page_w%3D%22).concat(n.page_width%2C%22%26page_h%3D%22).concat(n.page_height))%7Dcatch(t)%7Bl(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1002%2Cpos_err_m%3At.toString()%7D)%7Dvar%20e%3Dnew%20Image%3Be.src%3Dt%2Ce.style.display%3D%22none%22%2Ce.style.visibility%3D%22hidden%22%2Ce.width%3D0%2Ce.height%3D0%2Cdocument.body.appendChild(e)%7D)%7Dvar%20d%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D101%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BITRACKER2%7D%22%2C%22%24%7BITRACKER3%7D%22%2C%22%24%7BITRACKER4%7D%22%2C%22%24%7BITRACKER5%7D%22%2C%22%24%7BITRACKER6%7D%22%5D%2Cp%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D104%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26format%3D%26crid%3Dff32b6f9b3bbc45c00b78b6674a2952e%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BVTRACKER2%7D%22%2C%22%24%7BVTRACKER3%7D%22%2C%22%24%7BVTRACKER4%7D%22%2C%22%24%7BVTRACKER5%7D%22%2C%22%24%7BVTRACKER6%7D%22%5D%2Cs%3D%22f9f2b1ef23fe2759c2cad0953029a94b%22%2Cn%3Ddocument.getElementById(%22mgcontainer-99afea272c2b0e8626489674ddb7a0bb%22)%3Bn%26%26function()%7Bvar%20a%3Dn.getElementsByClassName(%22mediago-placement-track%22)%3Bif(a%26%26a.length)%7Bvar%20t%2Ce%3Dfunction(t)%7Bvar%20e%2Cn%2Co%2Ci%2Cc%2Cr%3B%22object%22%3D%3D%3Df(r%3Da%5Bt%5D)%26%26(e%3Dfunction(t)%7Btry%7Bvar%20e%3Dt.getBoundingClientRect()%2Cn%3De%26%26e.top%7C%7C-1%2Co%3De%26%26e.left%7C%7C-1%2Ci%3Ddocument.body.scrollWidth%7C%7C-1%2Ce%3Ddocument.body.scrollHeight%7C%7C-1%3Breturn%7Btop%3An.toFixed(0)%2Cleft%3Ao.toFixed(0)%2Cpage_width%3Ai%2Cpage_height%3Ae%7D%7Dcatch(o)%7Breturn%20l(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1001%2Cpos_err_m%3Ao.toString()%7D)%2C%7Btop%3A%22-1%22%2Cleft%3A%22-1%22%2Cpage_width%3A%22-1%22%2Cpage_height%3A%22-1%22%7D%7D%7D(r)%2C(n%3Dd%5Bt%5D)%26%26g(n%2C0%2Ce)%2Co%3Dp%5Bt%5D%2Ci%3D!1%2C(c%3Dfunction()%7BsetTimeout(function()%7Bvar%20t%2Ce%3B!i%26%26(t%3Dr%2Ce%3Dwindow.innerHeight%7C%7Cdocument.documentElement.clientHeight%7C%7Cdocument.body.clientHeight%2C(t.getBoundingClientRect()%26%26t.getBoundingClientRect().top)%3C%3De-.75*(t.offsetHeight%7C%7Ct.clientHeight))%3F(i%3D!0%2Co%26%26g(o))%3Ac()%7D%2C500)%7D)())%7D%3Bfor(t%20in%20a)e(t)%7D%7D()%7D()'; + temp += '%3B%3C%2Fscri'; + temp += 'pt%3E'; + adm += decodeURIComponent(temp); + let serverResponse = { + body: { + id: 'mgprebidjs_0b6572fc-ceba-418f-b6fd-33b41ad0ac8a', + seatbid: [ + { + bid: [ + { + id: '6e28cfaf115a354ea1ad8e1304d6d7b8', + impid: '1', + price: 0.087581, + adm: adm, + cid: '1339145', + crid: 'ff32b6f9b3bbc45c00b78b6674a2952e', + w: 300, + h: 250, + }, + ], + }, + ], + cur: 'USD', + }, + }; + + let bids = spec.interpretResponse(serverResponse); + // console.log({ + // bids + // }); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + + expect(bid.creativeId).to.equal('ff32b6f9b3bbc45c00b78b6674a2952e'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.currency).to.equal('USD'); + }); +}); From 9d8de1d468f46cb7863bf7ecf86d601e038869dc Mon Sep 17 00:00:00 2001 From: Alexander <32703851+pro-nsk@users.noreply.github.com> Date: Wed, 24 Aug 2022 23:11:20 +0700 Subject: [PATCH 22/25] Alkimi Bid Adapter: add adUnitCode parameter to bidder (#8897) * Alkimi bid adapter * Alkimi bid adapter * Alkimi bid adapter * alkimi adapter * onBidWon change * sign utils * auction ID as bid request ID * unit test fixes * change maintainer info * Updated the ad unit params * features support added * transfer adUnitCode * transfer adUnitCode: test Co-authored-by: Alexander Bogdanov Co-authored-by: Kalidas Engaiahraj Co-authored-by: mihanikw2g <92710748+mihanikw2g@users.noreply.github.com> Co-authored-by: Nikulin Mikhail --- modules/alkimiBidAdapter.js | 3 ++- test/spec/modules/alkimiBidAdapter_spec.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/alkimiBidAdapter.js b/modules/alkimiBidAdapter.js index ac8b5af9533..fe5a050f436 100644 --- a/modules/alkimiBidAdapter.js +++ b/modules/alkimiBidAdapter.js @@ -31,7 +31,8 @@ export const spec = { bidFloor: bidRequest.params.bidFloor, width: sizes[0].width, height: sizes[0].height, - impMediaType: getFormatType(bidRequest) + impMediaType: getFormatType(bidRequest), + adUnitCode: bidRequest.adUnitCode }) bidIds.push(bidRequest.bidId) }) diff --git a/test/spec/modules/alkimiBidAdapter_spec.js b/test/spec/modules/alkimiBidAdapter_spec.js index c98c81b706e..1ae9bb56df4 100644 --- a/test/spec/modules/alkimiBidAdapter_spec.js +++ b/test/spec/modules/alkimiBidAdapter_spec.js @@ -6,6 +6,7 @@ const REQUEST = { 'bidId': '456', 'bidder': 'alkimi', 'sizes': [[300, 250]], + 'adUnitCode': 'bannerAdUnitCode', 'mediaTypes': { 'banner': { 'sizes': [[300, 250]] @@ -138,7 +139,7 @@ describe('alkimiBidAdapter', function () { expect(bidderRequest.data.requestId).to.equal('123') expect(bidderRequest.data.referer).to.equal('http://test.com/path.html') expect(bidderRequest.data.schain).to.deep.contains({ver: '1.0', complete: 1, nodes: [{asi: 'alkimi-onboarding.com', sid: '00001', hp: 1}]}) - expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', pos: 7, bidFloor: 0.1, width: 300, height: 250, impMediaType: 'Banner' }) + expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', pos: 7, bidFloor: 0.1, width: 300, height: 250, impMediaType: 'Banner', adUnitCode: 'bannerAdUnitCode' }) expect(bidderRequest.data.signRequest.randomUUID).to.equal(undefined) expect(bidderRequest.data.bidIds).to.deep.contains('456') expect(bidderRequest.data.signature).to.equal(undefined) From d88091c7689c47f964d52ad1f8a23b9eea1f9fc3 Mon Sep 17 00:00:00 2001 From: prebidtappx <77485538+prebidtappx@users.noreply.github.com> Date: Wed, 24 Aug 2022 18:12:37 +0200 Subject: [PATCH 23/25] Tappx Bid Adapter: fix host info http regex (#8896) * Fix: creating host correctly when http or https are added from the beginning * Fix :: Changed double quotes for single quotes Co-authored-by: Jordi Arnau --- modules/tappxBidAdapter.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/tappxBidAdapter.js b/modules/tappxBidAdapter.js index 3cd77e7b853..11aa6c76c76 100644 --- a/modules/tappxBidAdapter.js +++ b/modules/tappxBidAdapter.js @@ -484,9 +484,18 @@ export function _getHostInfo(validBidRequests) { domainInfo.domain = hostParam.split('/', 1)[0]; + let regexHostParamHttps = new RegExp(`^https:\/\/`); + let regexHostParamHttp = new RegExp(`^http:\/\/`); + let regexNewEndpoints = new RegExp(`^(vz.*|zz.*)\\.[a-z]{3}\\.tappx\\.com$`, 'i'); let regexClassicEndpoints = new RegExp(`^([a-z]{3}|testing)\\.[a-z]{3}\\.tappx\\.com$`, 'i'); + if (regexHostParamHttps.test(hostParam)) { + hostParam = hostParam.replace('https://', ''); + } else if (regexHostParamHttp.test(hostParam)) { + hostParam = hostParam.replace('http://', ''); + } + if (regexNewEndpoints.test(domainInfo.domain)) { domainInfo.newEndpoint = true; domainInfo.endpoint = domainInfo.domain.split('.', 1)[0] From 5362745c08efcd374767f566a8c9bb1758093789 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 24 Aug 2022 12:38:29 -0600 Subject: [PATCH 24/25] Prebid core: optimize getRefererInfo to run only once per page (#8864) --- src/refererDetection.js | 13 ++++++++++++- test/spec/modules/enrichmentFpdModule_spec.js | 3 ++- test/spec/modules/fpdModule_spec.js | 3 ++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/refererDetection.js b/src/refererDetection.js index 15c080f5c69..28da182c7ab 100644 --- a/src/refererDetection.js +++ b/src/refererDetection.js @@ -11,6 +11,8 @@ import { config } from './config.js'; import {logWarn} from './utils.js'; +let RI = new WeakMap(); + /** * Prepend a URL with the page's protocol (http/https), if necessary. */ @@ -252,10 +254,19 @@ export function detectReferer(win) { }; } - return refererInfo; + return function() { + if (!RI.has(win)) { + RI.set(win, Object.freeze(refererInfo())); + } + return RI.get(win); + } } /** * @type {function(): refererInfo} */ export const getRefererInfo = detectReferer(window); + +export function resetRefererInfo() { + RI = new WeakMap(); +} diff --git a/test/spec/modules/enrichmentFpdModule_spec.js b/test/spec/modules/enrichmentFpdModule_spec.js index 7d7e463c015..3cc18f952cc 100644 --- a/test/spec/modules/enrichmentFpdModule_spec.js +++ b/test/spec/modules/enrichmentFpdModule_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { getRefererInfo } from 'src/refererDetection.js'; +import {getRefererInfo, resetRefererInfo} from 'src/refererDetection.js'; import { processFpd, coreStorage } from 'modules/enrichmentFpdModule.js'; describe('the first party data enrichment module', function() { @@ -20,6 +20,7 @@ describe('the first party data enrichment module', function() { }); beforeEach(function() { + resetRefererInfo(); querySelectorStub = sinon.stub(window.top.document, 'querySelector'); querySelectorStub.withArgs("link[rel='canonical']").returns(canonical); querySelectorStub.withArgs("meta[name='keywords']").returns(keywords); diff --git a/test/spec/modules/fpdModule_spec.js b/test/spec/modules/fpdModule_spec.js index 498bed29243..cf2ad4afe6f 100644 --- a/test/spec/modules/fpdModule_spec.js +++ b/test/spec/modules/fpdModule_spec.js @@ -1,6 +1,6 @@ import {expect} from 'chai'; import {config} from 'src/config.js'; -import {getRefererInfo} from 'src/refererDetection.js'; +import {getRefererInfo, resetRefererInfo} from 'src/refererDetection.js'; import {processFpd, registerSubmodules, startAuctionHook, reset} from 'modules/fpdModule/index.js'; import * as enrichmentModule from 'modules/enrichmentFpdModule.js'; import * as validationModule from 'modules/validationFpdModule/index.js'; @@ -70,6 +70,7 @@ describe('the first party data module', function () { }); beforeEach(function() { + resetRefererInfo(); querySelectorStub = sinon.stub(window.top.document, 'querySelector'); querySelectorStub.withArgs("link[rel='canonical']").returns(canonical); querySelectorStub.withArgs("meta[name='keywords']").returns(keywords); From 66fc005e1deaeee3fc1132ee324aee6efba54183 Mon Sep 17 00:00:00 2001 From: Jason Quaccia Date: Thu, 25 Aug 2022 03:52:32 -0700 Subject: [PATCH 25/25] Prebid Core: Batch Video Cache Requests feature (#8765) * batch video cache request integration * got rid of console log statement * removed video.html * addressed feedback * removed video file and uncommented test * reverted package-lock.json * retriggering circle ci * addressed changes * got rid of video html test file * removed unused methods from spec file * refactored auction.js and updated tests * updated test * removed video file * fixed test * moved logic where batch config options are referenced into the returned func of batchingCache * removed test video file * reverted a few other changes * moved batch config code outside of batchingCache func * always forget to remove this file after haha Co-authored-by: Jason Quaccia --- src/auction.js | 73 ++++++++++++++++++++++++++++-------- test/spec/videoCache_spec.js | 30 +++++++++++++++ 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src/auction.js b/src/auction.js index b7deeccfd82..da805ae41dd 100644 --- a/src/auction.js +++ b/src/auction.js @@ -514,28 +514,71 @@ function tryAddVideoBid(auctionInstance, bidResponse, afterBidAdded, {index = au } } -export const callPrebidCache = hook('async', function(auctionInstance, bidResponse, afterBidAdded, videoMediaType) { - store([bidResponse], function (error, cacheIds) { - if (error) { - logWarn(`Failed to save to the video cache: ${error}. Video bid must be discarded.`); - - doCallbacksIfTimedout(auctionInstance, bidResponse); - } else { - if (cacheIds[0].uuid === '') { - logWarn(`Supplied video cache key was already in use by Prebid Cache; caching attempt was rejected. Video bid must be discarded.`); +const storeInCache = (batch) => { + store(batch.map(entry => entry.bidResponse), function (error, cacheIds) { + cacheIds.forEach((cacheId, i) => { + const { auctionInstance, bidResponse, afterBidAdded } = batch[i]; + if (error) { + logWarn(`Failed to save to the video cache: ${error}. Video bid must be discarded.`); doCallbacksIfTimedout(auctionInstance, bidResponse); } else { - bidResponse.videoCacheKey = cacheIds[0].uuid; + if (cacheId.uuid === '') { + logWarn(`Supplied video cache key was already in use by Prebid Cache; caching attempt was rejected. Video bid must be discarded.`); - if (!bidResponse.vastUrl) { - bidResponse.vastUrl = getCacheUrl(bidResponse.videoCacheKey); + doCallbacksIfTimedout(auctionInstance, bidResponse); + } else { + bidResponse.videoCacheKey = cacheId.uuid; + + if (!bidResponse.vastUrl) { + bidResponse.vastUrl = getCacheUrl(bidResponse.videoCacheKey); + } + addBidToAuction(auctionInstance, bidResponse); + afterBidAdded(); } - addBidToAuction(auctionInstance, bidResponse); - afterBidAdded(); } - } + }); }); +}; + +let batchSize, batchTimeout; +config.getConfig('cache', (cacheConfig) => { + batchSize = typeof cacheConfig.cache.batchSize === 'number' && cacheConfig.cache.batchSize > 0 + ? cacheConfig.cache.batchSize + : 1; + batchTimeout = typeof cacheConfig.cache.batchTimeout === 'number' && cacheConfig.cache.batchTimeout > 0 + ? cacheConfig.cache.batchTimeout + : 0; +}); + +export const batchingCache = (timeout = setTimeout, cache = storeInCache) => { + let batches = [[]]; + let debouncing = false; + const noTimeout = cb => cb(); + + return function(auctionInstance, bidResponse, afterBidAdded) { + const batchFunc = batchTimeout > 0 ? timeout : noTimeout; + if (batches[batches.length - 1].length >= batchSize) { + batches.push([]); + } + + batches[batches.length - 1].push({auctionInstance, bidResponse, afterBidAdded}); + + if (!debouncing) { + debouncing = true; + batchFunc(() => { + batches.forEach(cache); + batches = [[]]; + debouncing = false; + }, batchTimeout); + } + } +}; + +const batchAndStore = batchingCache(); + +export const callPrebidCache = hook('async', function(auctionInstance, bidResponse, afterBidAdded, videoMediaType) { + batchAndStore(auctionInstance, bidResponse, afterBidAdded); }, 'callPrebidCache'); // Postprocess the bids so that all the universal properties exist, no matter which bidder they came from. diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index 5885dfb7cdf..a13028c966a 100644 --- a/test/spec/videoCache_spec.js +++ b/test/spec/videoCache_spec.js @@ -4,6 +4,7 @@ import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr.js'; import {auctionManager} from '../../src/auctionManager.js'; import {AuctionIndex} from '../../src/auctionIndex.js'; +import { batchingCache } from '../../src/auction.js'; const should = chai.should(); @@ -297,6 +298,35 @@ describe('The video cache', function () { JSON.parse(request.requestBody).should.deep.equal(payload); }); + it('should wait the duration of the batchTimeout and pass the correct batchSize if batched requests are enabled in the config', () => { + const mockAfterBidAdded = function() {}; + let callback = null; + let mockTimeout = sinon.stub().callsFake((cb) => { callback = cb }); + + config.setConfig({ + cache: { + url: 'https://prebid.adnxs.com/pbc/v1/cache', + batchSize: 3, + batchTimeout: 20 + } + }); + + let stubCache = sinon.stub(); + const batchAndStore = batchingCache(mockTimeout, stubCache); + for (let i = 0; i < 3; i++) { + batchAndStore({}, {}, mockAfterBidAdded); + } + + sinon.assert.calledOnce(mockTimeout); + sinon.assert.calledWith(mockTimeout, sinon.match.any, 20); + + const expectedBatch = [{ afterBidAdded: mockAfterBidAdded, auctionInstance: { }, bidResponse: { } }, { afterBidAdded: mockAfterBidAdded, auctionInstance: { }, bidResponse: { } }, { afterBidAdded: mockAfterBidAdded, auctionInstance: { }, bidResponse: { } }]; + + callback(); + + sinon.assert.calledWith(stubCache, expectedBatch); + }); + function assertRequestMade(bid, expectedValue) { store([bid], function () { });