From abad8101e8583bd86d9ead81d41cc35cef28e06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Udi=20Talias=20=E2=9A=9B=EF=B8=8F?= Date: Wed, 24 Jun 2020 22:17:46 +0300 Subject: [PATCH 01/39] Vidazoo Adapter: Feature/unit code (#5413) * feat(module): multi size request * fix getUserSyncs added tests * update(module): package-lock.json from master * feat(client): send adUnitCode on request payload Co-authored-by: roman --- modules/vidazooBidAdapter.js | 3 ++- test/spec/modules/vidazooBidAdapter_spec.js | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index cc955b14715..0cb5ad661e9 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -33,7 +33,7 @@ function isBidRequestValid(bid) { } function buildRequest(bid, topWindowUrl, sizes, bidderRequest) { - const { params, bidId, userId } = bid; + const { params, bidId, userId, adUnitCode } = bid; const { bidFloor, cId, pId, ext } = params; const hashUrl = hashCode(topWindowUrl); const dealId = getNextDealId(hashUrl); @@ -43,6 +43,7 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest) { cb: Date.now(), bidFloor: bidFloor, bidId: bidId, + adUnitCode: adUnitCode, publisherId: pId, sizes: sizes, dealId: dealId, diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index fb9c540708b..9441494fd88 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -4,6 +4,7 @@ import * as utils from 'src/utils.js'; const BID = { 'bidId': '2d52001cabd527', + 'adUnitCode': 'div-gpt-ad-12345-0', 'params': { 'cId': '59db6b3b4ffaa70004f45cdc', 'pId': '59ac17c192832d0011283fe3', @@ -138,6 +139,7 @@ describe('VidazooBidAdapter', function () { cb: 1000, bidFloor: 0.1, bidId: '2d52001cabd527', + adUnitCode: 'div-gpt-ad-12345-0', publisherId: '59ac17c192832d0011283fe3', dealId: 1, res: `${window.top.screen.width}x${window.top.screen.height}`, From d76ec12263eeb673dffc432c305f0d58a3f5ee7a Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Thu, 25 Jun 2020 02:54:28 -0500 Subject: [PATCH 02/39] Sovrn - Update Supported ID's, include adunitcode in ad request (#5403) * added tdid and ad-unit-code * fixed tdid * removed digitrust * repush * add package-lock from upstream master * Delete package-lock.json * add package-lock from upstream master Co-authored-by: Ankit Prakash Co-authored-by: Wesley Whitney Co-authored-by: John Rosendahl --- modules/sovrnBidAdapter.js | 36 ++++++++++++----------- test/spec/modules/sovrnBidAdapter_spec.js | 26 ++++++++-------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 98c8c8e3b33..f3260668b74 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -25,18 +25,13 @@ export const spec = { let sovrnImps = []; let iv; let schain; - let digitrust; + let unifiedID; utils._each(bidReqs, function (bid) { - if (!digitrust) { - const bidRequestDigitrust = utils.deepAccess(bid, 'userId.digitrustid.data'); - if (bidRequestDigitrust && (!bidRequestDigitrust.privacy || !bidRequestDigitrust.privacy.optout)) { - digitrust = { - id: bidRequestDigitrust.id, - keyv: bidRequestDigitrust.keyv - } - } + if (!unifiedID) { + unifiedID = utils.deepAccess(bid, 'userId.tdid'); } + if (bid.schain) { schain = schain || bid.schain; } @@ -47,6 +42,7 @@ export const spec = { bidSizes = bidSizes.filter(size => utils.isArray(size)) const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})) sovrnImps.push({ + adunitcode: bid.adUnitCode, id: bid.bidId, banner: { format: processedSizes, @@ -88,15 +84,22 @@ export const spec = { utils.deepSetValue(sovrnBidReq, 'regs.ext.us_privacy', bidderRequest.uspConsent); } - if (digitrust) { - utils.deepSetValue(sovrnBidReq, 'user.ext.digitrust', { - id: digitrust.id, - keyv: digitrust.keyv - }) + if (unifiedID) { + const idArray = [{ + source: 'adserver.org', + uids: [ + { + id: unifiedID, + ext: { + rtiPartner: 'TDID' + } + } + ] + }] + utils.deepSetValue(sovrnBidReq, 'user.ext.eids', idArray) } - let url = `https://ap.lijit.com/rtb/bid?` + - `src=$$REPO_AND_VERSION$$`; + let url = `https://ap.lijit.com/rtb/bid?src=$$REPO_AND_VERSION$$`; if (iv) url += `&iv=${iv}`; return { @@ -176,7 +179,6 @@ export const spec = { .forEach(url => tracks.push({ type: 'image', url })) } } - return tracks } catch (e) { return [] diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index c82cc32207a..54ccff870eb 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -70,12 +70,17 @@ describe('sovrnBidAdapter', function() { }); it('sets the proper banner object', function() { - const payload = JSON.parse(request.data); + const payload = JSON.parse(request.data) expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]) expect(payload.imp[0].banner.w).to.equal(1) expect(payload.imp[0].banner.h).to.equal(1) }) + it('includes the ad unit code int the request', function() { + const payload = JSON.parse(request.data); + expect(payload.imp[0].adunitcode).to.equal('adunit-code') + }) + it('accepts a single array as a size', function() { const singleSize = [{ 'bidder': 'sovrn', @@ -234,7 +239,7 @@ describe('sovrnBidAdapter', function() { expect(data.source.ext.schain.nodes.length).to.equal(1) }); - it('should add digitrust data if present', function() { + it('should add the unifiedID if present', function() { const digitrustRequests = [{ 'bidder': 'sovrn', 'params': { @@ -249,12 +254,7 @@ describe('sovrnBidAdapter', function() { 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', 'userId': { - 'digitrustid': { - 'data': { - 'id': 'digitrust-id-123', - 'keyv': 4 - } - } + 'tdid': 'SOMESORTOFID', } }].concat(bidRequests); const bidderRequest = { @@ -262,13 +262,13 @@ describe('sovrnBidAdapter', function() { referer: 'http://example.com/page.html', } }; - const data = JSON.parse(spec.buildRequests(digitrustRequests, bidderRequest).data); - expect(data.user.ext.digitrust.id).to.equal('digitrust-id-123'); - expect(data.user.ext.digitrust.keyv).to.equal(4); - }); + const data = JSON.parse(spec.buildRequests(digitrustRequests, bidderRequest).data); + expect(data.user.ext.eids[0].source).to.equal('adserver.org') + expect(data.user.ext.eids[0].uids[0].id).to.equal('SOMESORTOFID') + expect(data.user.ext.eids[0].uids[0].ext.rtiPartner).to.equal('TDID') + }) }); - describe('interpretResponse', function () { let response; beforeEach(function () { From 4e0df1ac3f7771ea0bdbf643043d94e4ef47a856 Mon Sep 17 00:00:00 2001 From: Corey Kress Date: Thu, 25 Jun 2020 09:31:02 -0400 Subject: [PATCH 03/39] [Synacormedia] adapter should use format for multi-size banner requests (#5410) * CAP-1614 - updated docs to show correct size for banner and some other small fixes * CAP-1636 support schain object in prebid * CAP-1636 updated the review comments * CAP-1849 - split up banner and video impressions to use format Co-authored-by: Corey Kress Co-authored-by: Rajkumar Natarajan --- modules/synacormediaBidAdapter.js | 106 ++++++++++++----- modules/synacormediaBidAdapter.md | 6 +- .../modules/synacormediaBidAdapter_spec.js | 112 +++++++++++------- 3 files changed, 148 insertions(+), 76 deletions(-) diff --git a/modules/synacormediaBidAdapter.js b/modules/synacormediaBidAdapter.js index 2ca2aeeae82..8f069f551ee 100644 --- a/modules/synacormediaBidAdapter.js +++ b/modules/synacormediaBidAdapter.js @@ -71,38 +71,18 @@ export const spec = { pos = 0; } const videoOrBannerKey = this.isVideoBid(bid) ? 'video' : 'banner'; - getAdUnitSizes(bid) - .filter(size => BLOCKED_AD_SIZES.indexOf(size.join('x')) === -1) - .forEach((size, i) => { - if (!size || size.length != 2) { - return; - } - const size0 = size[0]; - const size1 = size[1]; - const imp = { - id: `${videoOrBannerKey.substring(0, 1)}${bid.bidId}-${size0}x${size1}`, - tagid: placementId - }; - if (bidFloor !== null && !isNaN(bidFloor)) { - imp.bidfloor = bidFloor; - } + const adSizes = getAdUnitSizes(bid) + .filter(size => BLOCKED_AD_SIZES.indexOf(size.join('x')) === -1); - const videoOrBannerValue = { - w: size0, - h: size1, - pos - }; - if (videoOrBannerKey === 'video') { - if (bid.mediaTypes.video) { - this.setValidVideoParams(bid.mediaTypes.video, bid.params.video); - } - if (bid.params.video) { - this.setValidVideoParams(bid.params.video, videoOrBannerValue); - } - } - imp[videoOrBannerKey] = videoOrBannerValue; - openRtbBidRequest.imp.push(imp); - }); + let imps = []; + if (videoOrBannerKey === 'banner') { + imps = this.buildBannerImpressions(adSizes, bid, placementId, pos, bidFloor, videoOrBannerKey); + } else if (videoOrBannerKey === 'video') { + imps = this.buildVideoImpressions(adSizes, bid, placementId, pos, bidFloor, videoOrBannerKey); + } + if (imps.length > 0) { + imps.forEach(i => openRtbBidRequest.imp.push(i)); + } }); if (openRtbBidRequest.imp.length && seatId) { @@ -118,6 +98,70 @@ export const spec = { } }, + buildBannerImpressions: function(adSizes, bid, placementId, pos, bidFloor, videoOrBannerKey) { + let format = []; + let imps = []; + adSizes.forEach((size, i) => { + if (!size || size.length !== 2) { + return; + } + + format.push({ + w: size[0], + h: size[1], + }); + }); + + if (format.length > 0) { + const imp = { + id: `${videoOrBannerKey.substring(0, 1)}${bid.bidId}`, + banner: { + format, + pos + }, + tagid: placementId, + }; + if (bidFloor !== null && !isNaN(bidFloor)) { + imp.bidfloor = bidFloor; + } + imps.push(imp); + } + return imps; + }, + + buildVideoImpressions: function(adSizes, bid, placementId, pos, bidFloor, videoOrBannerKey) { + let imps = []; + adSizes.forEach((size, i) => { + if (!size || size.length != 2) { + return; + } + const size0 = size[0]; + const size1 = size[1]; + const imp = { + id: `${videoOrBannerKey.substring(0, 1)}${bid.bidId}-${size0}x${size1}`, + tagid: placementId + }; + if (bidFloor !== null && !isNaN(bidFloor)) { + imp.bidfloor = bidFloor; + } + + const videoOrBannerValue = { + w: size0, + h: size1, + pos + }; + if (bid.mediaTypes.video) { + this.setValidVideoParams(bid.mediaTypes.video, bid.params.video); + } + if (bid.params.video) { + this.setValidVideoParams(bid.params.video, videoOrBannerValue); + } + imp[videoOrBannerKey] = videoOrBannerValue; + imps.push(imp); + }); + return imps; + }, + setValidVideoParams: function (sourceObj, destObj) { Object.keys(sourceObj) .filter(param => includes(VIDEO_PARAMS, param) && sourceObj[param] !== null && (!isNaN(parseInt(sourceObj[param], 10)) || !(sourceObj[param].length < 1))) diff --git a/modules/synacormediaBidAdapter.md b/modules/synacormediaBidAdapter.md index 857cf15d240..fd71f07b3a3 100644 --- a/modules/synacormediaBidAdapter.md +++ b/modules/synacormediaBidAdapter.md @@ -42,8 +42,10 @@ https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_cache_id_synacorm code: 'test-div2', mediaTypes: { video: { - context: 'instream', - playerSize: [[300, 250]], + context: 'instream', + playerSize: [ + [300, 250] + ], } }, bids: [{ diff --git a/test/spec/modules/synacormediaBidAdapter_spec.js b/test/spec/modules/synacormediaBidAdapter_spec.js index f6fa7a20594..e15481d47e5 100644 --- a/test/spec/modules/synacormediaBidAdapter_spec.js +++ b/test/spec/modules/synacormediaBidAdapter_spec.js @@ -68,9 +68,13 @@ describe('synacormediaBidAdapter ', function () { }, mediaTypes: { banner: { - h: 600, - pos: 0, - w: 300, + format: [ + { + w: 300, + h: 600 + } + ], + pos: 0 } }, }; @@ -173,21 +177,19 @@ describe('synacormediaBidAdapter ', function () { let expectedDataImp1 = { banner: { - h: 250, - pos: 0, - w: 300, - }, - id: 'b9876abcd-300x250', - tagid: '1234', - bidfloor: 0.5 - }; - let expectedDataImp2 = { - banner: { - h: 600, - pos: 0, - w: 300, + format: [ + { + h: 250, + w: 300 + }, + { + h: 600, + w: 300 + } + ], + pos: 0 }, - id: 'b9876abcd-300x600', + id: 'b9876abcd', tagid: '1234', bidfloor: 0.5 }; @@ -201,7 +203,7 @@ describe('synacormediaBidAdapter ', function () { expect(req.url).to.contain('https://prebid.technoratimedia.com/openrtb/bids/prebid?'); expect(req.data).to.exist.and.to.be.an('object'); expect(req.data.id).to.equal('xyz123'); - expect(req.data.imp).to.eql([expectedDataImp1, expectedDataImp2]); + expect(req.data.imp).to.eql([expectedDataImp1]); // video test let reqVideo = spec.buildRequests([validBidRequestVideo], bidderRequestVideo); @@ -230,13 +232,17 @@ describe('synacormediaBidAdapter ', function () { expect(req).to.have.property('url'); expect(req.url).to.contain('https://prebid.technoratimedia.com/openrtb/bids/prebid?'); expect(req.data.id).to.equal('xyz123'); - expect(req.data.imp).to.eql([expectedDataImp1, expectedDataImp2, { + expect(req.data.imp).to.eql([expectedDataImp1, { banner: { - h: 600, - pos: 0, - w: 300, + format: [ + { + h: 600, + w: 300 + } + ], + pos: 0 }, - id: 'bfoobar-300x600', + id: 'bfoobar', tagid: '5678', bidfloor: 0.5 }]); @@ -260,11 +266,15 @@ describe('synacormediaBidAdapter ', function () { expect(req.data.imp).to.eql([ { banner: { - h: 250, - pos: 0, - w: 300, + format: [ + { + h: 250, + w: 300 + } + ], + pos: 0 }, - id: 'bfoobar-300x250', + id: 'bfoobar', tagid: '5678', bidfloor: 0.5 } @@ -289,11 +299,15 @@ describe('synacormediaBidAdapter ', function () { expect(req.data.imp).to.eql([ { banner: { - h: 250, - pos: 0, - w: 300, + format: [ + { + h: 250, + w: 300 + } + ], + pos: 0 }, - id: 'b9876abcd-300x250', + id: 'b9876abcd', tagid: '1234', } ]); @@ -316,11 +330,15 @@ describe('synacormediaBidAdapter ', function () { expect(req.data.imp).to.eql([ { banner: { - h: 250, - pos: 0, - w: 300, + format: [ + { + h: 250, + w: 300 + } + ], + pos: 0 }, - id: 'b9876abcd-300x250', + id: 'b9876abcd', tagid: '1234', } ]); @@ -344,11 +362,15 @@ describe('synacormediaBidAdapter ', function () { expect(req.data.imp).to.eql([ { banner: { - h: 250, - w: 300, - pos: 1, + format: [ + { + h: 250, + w: 300 + } + ], + pos: 1 }, - id: 'b9876abcd-300x250', + id: 'b9876abcd', tagid: '1234' } ]); @@ -371,11 +393,15 @@ describe('synacormediaBidAdapter ', function () { expect(req.data.imp).to.eql([ { banner: { - h: 250, - w: 300, - pos: 0, + format: [ + { + h: 250, + w: 300 + } + ], + pos: 0 }, - id: 'b9876abcd-300x250', + id: 'b9876abcd', tagid: '1234' } ]); From d14b9910a3cfbae58e5c4155ab643d0b3de29916 Mon Sep 17 00:00:00 2001 From: Jozef Bartek <31618107+jbartek25@users.noreply.github.com> Date: Thu, 25 Jun 2020 15:31:11 +0200 Subject: [PATCH 04/39] Improve Digital: adapter improvements (#5399) * Improve Digital: CCPA support * Outstream video support * Lint fixes * Improve Digital: outstream and deal improvements --- modules/improvedigitalBidAdapter.js | 30 ++++++++----------- .../modules/improvedigitalBidAdapter_spec.js | 9 ++++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index dfabf4eef55..3c000258ede 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -8,7 +8,7 @@ const BIDDER_CODE = 'improvedigital'; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; export const spec = { - version: '7.0.0', + version: '7.1.0', code: BIDDER_CODE, gvlid: 253, aliases: ['id'], @@ -123,7 +123,7 @@ export const spec = { // Deal ID. Composite ads can have multiple line items and the ID of the first // dealID line item will be used. - if (utils.isNumber(bidObject.lid) && bidObject.buying_type === 'deal_id') { + if (utils.isNumber(bidObject.lid) && bidObject.buying_type && bidObject.buying_type !== 'rtb') { bid.dealId = bidObject.lid; } else if (Array.isArray(bidObject.lid) && Array.isArray(bidObject.buying_type) && @@ -131,7 +131,7 @@ export const spec = { let isDeal = false; bidObject.buying_type.forEach((bt, i) => { if (isDeal) return; - if (bt === 'deal_id') { + if (bt && bt !== 'rtb') { isDeal = true; bid.dealId = bidObject.lid[i]; } @@ -182,9 +182,10 @@ export const spec = { }; function isInstreamVideo(bid) { + const mediaTypes = Object.keys(utils.deepAccess(bid, 'mediaTypes', {})); const videoMediaType = utils.deepAccess(bid, 'mediaTypes.video'); const context = utils.deepAccess(bid, 'mediaTypes.video.context'); - return bid.mediaType === 'video' || (videoMediaType && context !== 'outstream'); + return bid.mediaType === 'video' || (mediaTypes.length === 1 && videoMediaType && context !== 'outstream'); } function isOutstreamVideo(bid) { @@ -197,30 +198,23 @@ function outstreamRender(bid) { bid.renderer.push(() => { window.ANOutstreamVideo.renderAd({ sizes: [bid.width, bid.height], - width: bid.width, - height: bid.height, targetId: bid.adUnitCode, adResponse: bid.adResponse, - rendererOptions: { - showBigPlayButton: false, - showProgressBar: 'bar', - showVolume: false, - allowFullscreen: true, - skippable: false, - } - }); + rendererOptions: bid.renderer.getConfig() + }, handleOutstreamRendererEvents.bind(null, bid)); }); } +function handleOutstreamRendererEvents(bid, id, eventName) { + bid.renderer.handleVideoEvent({ id, eventName }); +} + function createRenderer(bidRequest) { const renderer = Renderer.install({ id: bidRequest.adUnitCode, url: RENDERER_URL, loaded: false, - config: { - player_width: bidRequest.mediaTypes.video.playerSize[0][0], - player_height: bidRequest.mediaTypes.video.playerSize[0][1] - }, + config: utils.deepAccess(bidRequest, 'renderer.options'), adUnitCode: bidRequest.adUnitCode }); try { diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 1466b509c54..5a20944a6ed 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -778,10 +778,15 @@ describe('Improve Digital Adapter Tests', function () { expect(bids[0].dealId).to.not.exist; response.body.bid[0].lid = 268515; - response.body.bid[0].buying_type = 'classic'; + response.body.bid[0].buying_type = 'rtb'; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.not.exist; + response.body.bid[0].lid = 268515; + response.body.bid[0].buying_type = 'classic'; + bids = spec.interpretResponse(response, {bidderRequest}); + expect(bids[0].dealId).to.equal(268515); + response.body.bid[0].lid = 268515; response.body.bid[0].buying_type = 'deal_id'; bids = spec.interpretResponse(response, {bidderRequest}); @@ -798,7 +803,7 @@ describe('Improve Digital Adapter Tests', function () { expect(bids[0].dealId).to.not.exist; response.body.bid[0].lid = [ 268515, 12456, 34567 ]; - response.body.bid[0].buying_type = [ 'classic', 'deal_id', 'deal_id' ]; + response.body.bid[0].buying_type = [ 'rtb', 'deal_id', 'deal_id' ]; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.equal(12456); }); From 9186d64daf4961b9fa06de3b33e9f95fb2053ccc Mon Sep 17 00:00:00 2001 From: guiann Date: Thu, 25 Jun 2020 15:31:19 +0200 Subject: [PATCH 05/39] Ayl gdp rdefault value (#5391) * Remove useless bidderCode in bid response * send all the available sizes in the bid request * Use the banner sizes if given * avoid compatibility issue with old bid format * Remove gdpr default apply value * minor: use better variable name * Add unit test on unspecified gdprApplies Co-authored-by: Guillaume --- modules/adyoulikeBidAdapter.js | 4 ++-- test/spec/modules/adyoulikeBidAdapter_spec.js | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 412acdde3dd..725ee0f5626 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -48,7 +48,7 @@ export const spec = { if (bidderRequest && bidderRequest.gdprConsent) { payload.gdprConsent = { consentString: bidderRequest.gdprConsent.consentString, - consentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : true + consentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : null }; } @@ -80,7 +80,7 @@ export const spec = { try { bidRequests = JSON.parse(request.data).Bids; - } catch (e) { + } catch (err) { // json error initial request can't be read } diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index 3573681dd17..d2d4e10c17f 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -275,6 +275,29 @@ describe('Adyoulike Adapter', function () { expect(payload.uspConsent).to.exist.and.to.equal(uspConsentData); }); + it('should not set a default value for gdpr consentRequired', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let uspConsentData = '1YCC'; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString + }, + 'uspConsent': uspConsentData + }; + + bidderRequest.bids = bidRequestWithSinglePlacement; + + const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdprConsent).to.exist; + expect(payload.gdprConsent.consentString).to.exist.and.to.equal(consentString); + expect(payload.gdprConsent.consentRequired).to.be.null; + }); + it('sends bid request to endpoint with single placement', function () { const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); const payload = JSON.parse(request.data); From 0dd9faa7646a5175eccc0b2a5c936936f67e52ee Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Thu, 25 Jun 2020 12:22:00 -0700 Subject: [PATCH 06/39] Price floors new schema support AB Test (#5390) * Price floors new schema support AB Test * Add new serve-fast command + lint fix * update comment * Only sum up modelWeights once and set as prop! Fix minor bug in handleFetchResponse to overwrite skipRate --- gulpfile.js | 13 +- modules/priceFloors.js | 63 ++++++++- test/spec/modules/priceFloors_spec.js | 186 ++++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 10 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 58e294bc559..64152baa7ba 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -341,12 +341,12 @@ function injectFakeServerEndpointDev() { function startFakeServer() { const fakeServer = spawn('node', ['./test/fake-server/index.js', `--port=${FAKE_SERVER_PORT}`]); - fakeServer.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); - }); - fakeServer.stderr.on('data', (data) => { - console.log(`stderr: ${data}`); - }); + fakeServer.stdout.on('data', (data) => { + console.log(`stdout: ${data}`); + }); + fakeServer.stderr.on('data', (data) => { + console.log(`stderr: ${data}`); + }); } // support tasks @@ -372,6 +372,7 @@ gulp.task('build', gulp.series(clean, 'build-bundle-prod')); gulp.task('build-postbid', gulp.series(escapePostbidConfig, buildPostbid)); gulp.task('serve', gulp.series(clean, lint, gulp.parallel('build-bundle-dev', watch, test))); +gulp.task('serve-fast', gulp.series(clean, gulp.parallel('build-bundle-dev', watch))); gulp.task('serve-fake', gulp.series(clean, gulp.parallel('build-bundle-dev', watch), injectFakeServerEndpointDev, test, startFakeServer)); gulp.task('default', gulp.series(clean, makeWebpackPkg)); diff --git a/modules/priceFloors.js b/modules/priceFloors.js index e84576d3474..6d35a0a74cc 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -295,11 +295,29 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { }); } +export function pickRandomModel(modelGroups, weightSum) { + // we loop through the models subtracting the current model weight from our random number + // once we are at or below zero, we return the associated model + let random = Math.floor(Math.random() * weightSum + 1) + for (let i = 0; i < modelGroups.length; i++) { + random -= modelGroups[i].modelWeight; + if (random <= 0) { + return modelGroups[i]; + } + } +}; + /** * @summary Updates the adUnits accordingly and returns the necessary floorsData for the current auction */ export function createFloorsDataForAuction(adUnits, auctionId) { let resolvedFloorsData = utils.deepClone(_floorsConfig); + // if using schema 2 pick a model here: + if (utils.deepAccess(resolvedFloorsData, 'data.floorsSchemaVersion') === 2) { + // merge the models specific stuff into the top level data settings (now it looks like floorsSchemaVersion 1!) + let { modelGroups, ...rest } = resolvedFloorsData.data; + resolvedFloorsData.data = Object.assign(rest, pickRandomModel(modelGroups, rest.modelWeightSum)); + } // if we do not have a floors data set, we will try to use data set on adUnits let useAdUnitData = Object.keys(utils.deepAccess(resolvedFloorsData, 'data.values') || {}).length === 0; @@ -372,6 +390,36 @@ function validateRules(floorsData, numFields, delimiter) { return Object.keys(floorsData.values).length > 0; } +function modelIsValid(model) { + // schema.fields has only allowed attributes + if (!validateSchemaFields(utils.deepAccess(model, 'schema.fields'))) { + return false; + } + return validateRules(model, model.schema.fields.length, model.schema.delimiter || '|') +} + +/** + * @summary Mapping of floor schema version to it's corresponding validation + */ +const floorsSchemaValidation = { + 1: data => modelIsValid(data), + 2: data => { + // model groups should be an array with at least one element + if (!Array.isArray(data.modelGroups) || data.modelGroups.length === 0) { + return false; + } + // every model should have valid schema, as well as an accompanying modelWeight + data.modelWeightSum = 0; + return data.modelGroups.every(model => { + if (typeof model.modelWeight === 'number' && modelIsValid(model)) { + data.modelWeightSum += model.modelWeight; + return true; + } + return false; + }); + } +}; + /** * @summary Fields array should have at least one entry and all should match allowed fields * Each rule in the values array should have a 'key' and 'floor' param @@ -382,11 +430,12 @@ export function isFloorsDataValid(floorsData) { if (typeof floorsData !== 'object') { return false; } - // schema.fields has only allowed attributes - if (!validateSchemaFields(utils.deepAccess(floorsData, 'schema.fields'))) { + floorsData.floorsSchemaVersion = floorsData.floorsSchemaVersion || 1; + if (typeof floorsSchemaValidation[floorsData.floorsSchemaVersion] !== 'function') { + utils.logError(`${MODULE_NAME}: Unknown floorsSchemaVersion: `, floorsData.floorsSchemaVersion); return false; } - return validateRules(floorsData, floorsData.schema.fields.length, floorsData.schema.delimiter || '|') + return floorsSchemaValidation[floorsData.floorsSchemaVersion](floorsData); } /** @@ -458,7 +507,13 @@ export function handleFetchResponse(fetchResponse) { floorResponse = fetchResponse; } // Update the global floors object according to the fetched data - _floorsConfig.data = parseFloorData(floorResponse, 'fetch') || _floorsConfig.data; + const fetchData = parseFloorData(floorResponse, 'fetch'); + if (fetchData) { + // set .data to it + _floorsConfig.data = fetchData; + // set skipRate override if necessary + _floorsConfig.skipRate = utils.isNumber(fetchData.skipRate) ? fetchData.skipRate : _floorsConfig.skipRate; + } // if any auctions are waiting for fetch to finish, we need to continue them! resumeDelayedAuctions(); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index 2a816aef104..9d35554c27f 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -407,6 +407,89 @@ describe('the price floors module', function () { fetchStatus: undefined }); }); + it('should randomly pick a model if floorsSchemaVersion is 2', function () { + let inputFloors = { + ...basicFloorConfig, + data: { + floorsSchemaVersion: 2, + currency: 'USD', + modelGroups: [ + { + modelVersion: 'model-1', + modelWeight: 10, + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + '*': 2.5 + } + }, { + modelVersion: 'model-2', + modelWeight: 40, + schema: { + delimiter: '|', + fields: ['size'] + }, + values: { + '300x250': 1.0, + '*': 2.5 + } + }, { + modelVersion: 'model-3', + modelWeight: 50, + schema: { + delimiter: '|', + fields: ['domain'] + }, + values: { + 'www.prebid.org': 1.0, + '*': 2.5 + } + } + ] + } + }; + handleSetFloorsConfig(inputFloors); + + // stub random to give us wanted vals + let randValue; + sandbox.stub(Math, 'random').callsFake(() => randValue); + + // 0 - 10 should use first model + randValue = 0.05; + runStandardAuction(); + validateBidRequests(true, { + skipped: false, + modelVersion: 'model-1', + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined + }); + + // 11 - 50 should use second model + randValue = 0.40; + runStandardAuction(); + validateBidRequests(true, { + skipped: false, + modelVersion: 'model-2', + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined + }); + + // 51 - 100 should use third model + randValue = 0.75; + runStandardAuction(); + validateBidRequests(true, { + skipped: false, + modelVersion: 'model-3', + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined + }); + }); it('should not overwrite previous data object if the new one is bad', function () { handleSetFloorsConfig({...basicFloorConfig}); handleSetFloorsConfig({ @@ -547,6 +630,44 @@ describe('the price floors module', function () { fetchStatus: 'success' }); }); + it('it should correctly overwrite skipRate with fetch skipRate', function () { + // so floors does not skip + sandbox.stub(Math, 'random').callsFake(() => 0.99); + // init the fake server with response stuff + let fetchFloorData = { + ...basicFloorData, + modelVersion: 'fetch model name', // change the model name + }; + fetchFloorData.skipRate = 95; + fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + + // run setConfig indicating fetch + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + + // floor provider should be called + expect(fakeFloorProvider.requests.length).to.equal(1); + expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + + // start the auction it should delay and not immediately call `continueAuction` + runStandardAuction(); + + // exposedAdUnits should be undefined if the auction has not continued + expect(exposedAdUnits).to.be.undefined; + + // make the fetch respond + fakeFloorProvider.respond(); + expect(exposedAdUnits).to.not.be.undefined; + + // the exposedAdUnits should be from the fetch not setConfig level data + // and fetchStatus is success since fetch worked + validateBidRequests(true, { + skipped: false, + modelVersion: 'fetch model name', + location: 'fetch', + skipRate: 95, + fetchStatus: 'success' + }); + }); it('Should not break if floor provider returns 404', function () { // run setConfig indicating fetch handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); @@ -615,6 +736,11 @@ describe('the price floors module', function () { expect(logErrorSpy.calledOnce).to.equal(true); }); describe('isFloorsDataValid', function () { + it('should return false if unknown floorsSchemaVersion', function () { + let inputFloorData = utils.deepClone(basicFloorData); + inputFloorData.floorsSchemaVersion = 3; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + }); it('should work correctly for fields array', function () { let inputFloorData = utils.deepClone(basicFloorData); expect(isFloorsDataValid(inputFloorData)).to.to.equal(true); @@ -670,6 +796,66 @@ describe('the price floors module', function () { expect(isFloorsDataValid(inputFloorData)).to.to.equal(true); expect(inputFloorData.values).to.deep.equal({ 'test-div-1|native': 1.0 }); }); + it('should work correctly for floorsSchemaVersion 2', function () { + let inputFloorData = { + floorsSchemaVersion: 2, + currency: 'USD', + modelGroups: [ + { + modelVersion: 'model-1', + modelWeight: 10, + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + '*': 2.5 + } + }, { + modelVersion: 'model-2', + modelWeight: 40, + schema: { + delimiter: '|', + fields: ['size'] + }, + values: { + '300x250': 1.0, + '*': 2.5 + } + }, { + modelVersion: 'model-3', + modelWeight: 50, + schema: { + delimiter: '|', + fields: ['domain'] + }, + values: { + 'www.prebid.org': 1.0, + '*': 2.5 + } + } + ] + }; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(true); + + // remove one of the modelWeight's and it should be false + delete inputFloorData.modelGroups[1].modelWeight; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + inputFloorData.modelGroups[1].modelWeight = 40; + + // remove values from a model and it should not validate + const tempValues = {...inputFloorData.modelGroups[0].values}; + delete inputFloorData.modelGroups[0].values; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + inputFloorData.modelGroups[0].values = tempValues; + + // modelGroups should be an array and have at least one entry + delete inputFloorData.modelGroups; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + inputFloorData.modelGroups = []; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + }); }); describe('getFloor', function () { let bidRequest = { From 03bd2d3b6f18629eab217aa3371047ea6142d45c Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 25 Jun 2020 12:29:53 -0700 Subject: [PATCH 07/39] Prebid 3.24.0 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f08b7356fa..c203d896ddc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.24.0-pre", + "version": "3.24.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 81d0c58b9ce7144fb5dfa64ff31f028af8ef81d2 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 25 Jun 2020 13:28:35 -0700 Subject: [PATCH 08/39] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c203d896ddc..db35edf3b9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.24.0", + "version": "3.25.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 240e605e2d5bb88987f598c46c4df7df37a9d7da Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 29 Jun 2020 00:47:14 -0700 Subject: [PATCH 09/39] Removing Digitrust related test case for PubMatic bidder (#5426) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * removed digitrust test case --- test/spec/modules/pubmaticBidAdapter_spec.js | 41 -------------------- 1 file changed, 41 deletions(-) diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 45259767133..7a33df6fdfa 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -1350,47 +1350,6 @@ describe('PubMatic adapter', function () { }); }); - describe('Digitrust Id', function() { - it('send the digitrust id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.digitrustid = {data: {id: 'digitrust_user_id'}}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'digitru.st', - 'uids': [{ - 'id': 'digitrust_user_id', - 'atype': 1 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.digitrustid = {data: {id: 1}}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.digitrustid = {data: {id: []}}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.digitrustid = {data: {id: null}}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.digitrustid = {data: {id: {}}}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - describe('ID5 Id', function() { it('send the id5 id if it is present', function() { bidRequests[0].userId = {}; From 2a06f084343d413b7ec9a9e55219bd6df188190a Mon Sep 17 00:00:00 2001 From: Pablo Brud Date: Mon, 29 Jun 2020 12:31:47 -0300 Subject: [PATCH 10/39] CCPA modifications in the NextRoll adapter (#5409) * Add native support * Add response testing * DRY test * Change required from bool to int * Set mediaType * Fixes objects * Fixes object access * Remove ad property, only set it for banner * Update tests * Moving hardcoding values to constants * Update docs with native information * Revert "Add native support" * Getting rid of CCPA adapter validation (#9) * fix linter errors (#10) Co-authored-by: Ricardo Azpeitia Pimentel Co-authored-by: Abimael Martinez --- modules/nextrollBidAdapter.js | 144 +++++++++---------- test/spec/modules/nextrollBidAdapter_spec.js | 46 +----- 2 files changed, 75 insertions(+), 115 deletions(-) diff --git a/modules/nextrollBidAdapter.js b/modules/nextrollBidAdapter.js index 56fca22d2c3..02ebcd3f87a 100644 --- a/modules/nextrollBidAdapter.js +++ b/modules/nextrollBidAdapter.js @@ -30,12 +30,12 @@ export const spec = { */ buildRequests: function (validBidRequests, bidderRequest) { let topLocation = utils.parseUrl(utils.deepAccess(bidderRequest, 'refererInfo.referer')); - let consent = hasCCPAConsent(bidderRequest); - return validBidRequests.map((bidRequest, index) => { + + return validBidRequests.map((bidRequest) => { return { method: 'POST', options: { - withCredentials: consent, + withCredentials: true, }, url: BIDDER_ENDPOINT, data: { @@ -59,9 +59,10 @@ export const spec = { site: _getSite(bidRequest, topLocation), seller: _getSeller(bidRequest), device: _getDevice(bidRequest), + regs: _getRegs(bidderRequest) } - } - }) + }; + }); }, /** @@ -82,22 +83,22 @@ export const spec = { } function _getBanner(bidRequest) { - let sizes = _getSizes(bidRequest) - if (sizes === undefined) return undefined - return {format: sizes} + let sizes = _getSizes(bidRequest); + if (sizes === undefined) return undefined; + return {format: sizes}; } function _getNative(mediaTypeNative) { - if (mediaTypeNative === undefined) return undefined - let assets = _getNativeAssets(mediaTypeNative) - if (assets === undefined || assets.length == 0) return undefined + if (mediaTypeNative === undefined) return undefined; + let assets = _getNativeAssets(mediaTypeNative); + if (assets === undefined || assets.length == 0) return undefined; return { request: { native: { assets: assets } } - } + }; } /* @@ -114,44 +115,44 @@ const NATIVE_ASSET_MAP = [ {id: 4, kind: 'img', key: 'logo', type: 2}, {id: 5, kind: 'data', key: 'sponsoredBy', type: 1}, {id: 6, kind: 'data', key: 'body', type: 2} -] +]; const ASSET_KIND_MAP = { title: _getTitleAsset, img: _getImageAsset, data: _getDataAsset, -} +}; function _getAsset(mediaTypeNative, assetMap) { - let asset = mediaTypeNative[assetMap.key] - if (asset === undefined) return undefined - let assetFunc = ASSET_KIND_MAP[assetMap.kind] + const asset = mediaTypeNative[assetMap.key]; + if (asset === undefined) return undefined; + const assetFunc = ASSET_KIND_MAP[assetMap.kind]; return { id: assetMap.id, required: (assetMap.required || !!asset.required) ? 1 : 0, [assetMap.kind]: assetFunc(asset, assetMap) - } + }; } function _getTitleAsset(title, _assetMap) { - return {len: title.len || 0} + return {len: title.len || 0}; } function _getMinAspectRatio(aspectRatio, property) { - if (!utils.isPlainObject(aspectRatio)) return 1 + if (!utils.isPlainObject(aspectRatio)) return 1; - let ratio = aspectRatio['ratio_' + property] - let min = aspectRatio['min_' + property] + const ratio = aspectRatio['ratio_' + property]; + const min = aspectRatio['min_' + property]; - if (utils.isNumber(ratio)) return ratio - if (utils.isNumber(min)) return min + if (utils.isNumber(ratio)) return ratio; + if (utils.isNumber(min)) return min; - return 1 + return 1; } function _getImageAsset(image, assetMap) { - let sizes = image.sizes - let aspectRatio = image.aspect_ratios ? image.aspect_ratios[0] : undefined + const sizes = image.sizes; + const aspectRatio = image.aspect_ratios ? image.aspect_ratios[0] : undefined; return { type: assetMap.type, @@ -159,24 +160,26 @@ function _getImageAsset(image, assetMap) { h: (sizes ? sizes[1] : undefined), wmin: _getMinAspectRatio(aspectRatio, 'width'), hmin: _getMinAspectRatio(aspectRatio, 'height'), - } + }; } function _getDataAsset(data, assetMap) { return { type: assetMap.type, len: data.len || 0 - } + }; } function _getNativeAssets(mediaTypeNative) { - return NATIVE_ASSET_MAP.map(assetMap => _getAsset(mediaTypeNative, assetMap)).filter(asset => asset !== undefined) + return NATIVE_ASSET_MAP + .map(assetMap => _getAsset(mediaTypeNative, assetMap)) + .filter(asset => asset !== undefined); } function _getUser(requests) { - let id = utils.deepAccess(requests, '0.userId.nextroll'); + const id = utils.deepAccess(requests, '0.userId.nextroll'); if (id === undefined) { - return + return; } return { @@ -186,7 +189,7 @@ function _getUser(requests) { id }] } - } + }; } function _buildResponse(bidResponse, bid) { @@ -200,15 +203,15 @@ function _buildResponse(bidResponse, bid) { currency: 'USD', netRevenue: true, ttl: 300 - } + }; if (utils.isStr(bid.adm)) { - response.mediaType = BANNER - response.ad = utils.replaceAuctionPrice(bid.adm, bid.price) + response.mediaType = BANNER; + response.ad = utils.replaceAuctionPrice(bid.adm, bid.price); } else { - response.mediaType = NATIVE - response.native = _getNativeResponse(bid.adm, bid.price) + response.mediaType = NATIVE; + response.native = _getNativeResponse(bid.adm, bid.price); } - return response + return response; } const privacyLink = 'https://info.evidon.com/pub_info/573'; @@ -222,30 +225,30 @@ function _getNativeResponse(adm, price) { impressionTrackers: adm.imptrackers.map(impTracker => utils.replaceAuctionPrice(impTracker, price)), privacyLink: privacyLink, privacyIcon: privacyIcon - } + }; return adm.assets.reduce((accResponse, asset) => { - let assetMaps = NATIVE_ASSET_MAP.filter(assetMap => assetMap.id === asset.id && asset[assetMap.kind] !== undefined) - if (assetMaps.length === 0) return accResponse - let assetMap = assetMaps[0] - accResponse[assetMap.key] = _getAssetResponse(asset, assetMap) - return accResponse - }, baseResponse) + const assetMaps = NATIVE_ASSET_MAP.filter(assetMap => assetMap.id === asset.id && asset[assetMap.kind] !== undefined); + if (assetMaps.length === 0) return accResponse; + const assetMap = assetMaps[0]; + accResponse[assetMap.key] = _getAssetResponse(asset, assetMap); + return accResponse; + }, baseResponse); } function _getAssetResponse(asset, assetMap) { switch (assetMap.kind) { case 'title': - return asset.title.text + return asset.title.text; case 'img': return { url: asset.img.url, width: asset.img.w, height: asset.img.h - } + }; case 'data': - return asset.data.value + return asset.data.value; } } @@ -256,25 +259,25 @@ function _getSite(bidRequest, topLocation) { publisher: { id: utils.getBidIdParameter('publisherId', bidRequest.params) } - } + }; } function _getSeller(bidRequest) { return { id: utils.getBidIdParameter('sellerId', bidRequest.params) - } + }; } function _getSizes(bidRequest) { if (!utils.isArray(bidRequest.sizes)) { - return undefined + return undefined; } return bidRequest.sizes.filter(_isValidSize).map(size => { return { w: size[0], h: size[1] } - }) + }); } function _isValidSize(size) { @@ -288,7 +291,18 @@ function _getDevice(_bidRequest) { language: navigator['language'], os: _getOs(navigator.userAgent.toLowerCase()), osv: _getOsVersion(navigator.userAgent) + }; +} + +function _getRegs(bidderRequest) { + if (!bidderRequest || !bidderRequest.uspConsent) { + return undefined; } + return { + ext: { + us_privacy: bidderRequest.uspConsent + } + }; } function _getOs(userAgent) { @@ -308,7 +322,7 @@ function _getOs(userAgent) { } function _getOsVersion(userAgent) { - let clientStrings = [ + const clientStrings = [ { s: 'Android', r: /Android/ }, { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, { s: 'Mac OS X', r: /Mac OS X/ }, @@ -328,26 +342,4 @@ function _getOsVersion(userAgent) { return cs ? cs.s : 'unknown'; } -export function hasCCPAConsent(bidderRequest) { - if (bidderRequest === undefined) return true; - if (typeof bidderRequest.uspConsent !== 'string') { - return true; - } - const usps = bidderRequest.uspConsent; - const version = usps[0]; - - // If we don't support the consent string, assume no-consent. - if (version !== '1' || usps.length < 3) { - return false; - } - - const notice = usps[1]; - const optOut = usps[2]; - - if (notice === 'N' || optOut === 'Y') { - return false; - } - return true; -} - registerBidder(spec); diff --git a/test/spec/modules/nextrollBidAdapter_spec.js b/test/spec/modules/nextrollBidAdapter_spec.js index e1d85244931..7722443e584 100644 --- a/test/spec/modules/nextrollBidAdapter_spec.js +++ b/test/spec/modules/nextrollBidAdapter_spec.js @@ -124,6 +124,13 @@ describe('nextrollBidAdapter', function() { expect(bannerObject.format[0].w).to.be.equal(300); expect(bannerObject.format[0].h).to.be.equal(200); }); + + it('sets the CCPA consent string', function () { + const us_privacy = '1YYY'; + const request = spec.buildRequests([validBid], {'uspConsent': us_privacy})[0]; + + expect(request.data.regs.ext.us_privacy).to.be.equal(us_privacy); + }); }); describe('interpretResponse', function () { @@ -258,43 +265,4 @@ describe('nextrollBidAdapter', function() { expect(response[0].native).to.be.deep.equal(expectedResponse) }) }) - - describe('hasCCPAConsent', function() { - function ccpaRequest(consentString) { - return { - bidderCode: 'bidderX', - auctionId: 'e3a336ad-2222-4a1c-bbbb-ecc7c5554a34', - uspConsent: consentString - }; - } - - const noNoticeCases = ['1NYY', '1NNN', '1N--']; - noNoticeCases.forEach((ccpaString, index) => { - it(`No notice should indicate no consent (case ${index})`, function () { - const req = ccpaRequest(ccpaString); - expect(hasCCPAConsent(req)).to.be.false; - }); - }); - - const noConsentCases = ['1YYY', '1YYN', '1YY-']; - noConsentCases.forEach((ccpaString, index) => { - it(`Opt-Out should indicate no consent (case ${index})`, function () { - const req = ccpaRequest(ccpaString); - expect(hasCCPAConsent(req)).to.be.false; - }); - }); - - const consentCases = [undefined, '1YNY', '1YN-', '1Y--', '1---']; - consentCases.forEach((ccpaString, index) => { - it(`should indicate consent (case ${index})`, function() { - const req = ccpaRequest(ccpaString); - expect(hasCCPAConsent(req)).to.be.true; - }) - }); - - it('builds a request with no credentials', function () { - const noConsent = ccpaRequest('1YYY'); - expect(spec.buildRequests([validBid], noConsent)[0].options.withCredentials).to.be.false; - }); - }); }); From ccf10b6dd5b0b3e41d01a35198ab7a83e2ce4df1 Mon Sep 17 00:00:00 2001 From: Rich Audience Date: Mon, 29 Jun 2020 20:21:47 +0200 Subject: [PATCH 11/39] Add Render RichAudience Adapter (#5357) * Add Render RichAudience Adapter * Update richaudienceBidAdapter.md & Add Try/Catch Co-authored-by: sgimenez --- modules/richaudienceBidAdapter.js | 55 ++++++-- modules/richaudienceBidAdapter.md | 31 ++++- .../modules/richaudienceBidAdapter_spec.js | 118 +++++++++++++++++- 3 files changed, 188 insertions(+), 16 deletions(-) diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 22db9709c7c..43bef356a73 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -2,6 +2,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import * as utils from '../src/utils.js'; +import { Renderer } from '../src/Renderer.js'; const BIDDER_CODE = 'richaudience'; let REFERER = ''; @@ -46,7 +47,7 @@ export const spec = { transactionId: bid.transactionId, timeout: config.getConfig('bidderTimeout'), user: raiSetEids(bid), - demand: raiGetDemandType(bid) ? 'video' : 'display', + demand: raiGetDemandType(bid), videoData: raiGetVideoInfo(bid) }; @@ -97,8 +98,18 @@ export const spec = { if (response.media_type === 'video') { bidResponse.vastXml = response.vastXML; + try { + if (JSON.parse(bidRequest.data).videoData.format == 'outstream') { + bidResponse.renderer = Renderer.install({ + url: 'https://cdn3.richaudience.com/prebidVideo/player.js' + }); + bidResponse.renderer.setRender(renderer); + } + } catch (e) { + bidResponse.ad = response.adm; + } } else { - bidResponse.ad = response.adm + bidResponse.ad = response.adm; } bidResponses.push(bidResponse); @@ -121,7 +132,7 @@ export const spec = { var consent = ''; if (gdprConsent && typeof gdprConsent.consentString === 'string' && typeof gdprConsent.consentString != 'undefined') { - consent = `pubconsent='${gdprConsent.consentString}'&euconsent='${gdprConsent.consentString}'` + consent = `consentString=’${gdprConsent.consentString}` } if (syncOptions.iframeEnabled) { @@ -165,20 +176,23 @@ function raiGetSizes(bid) { } function raiGetDemandType(bid) { + let raiFormat = 'display'; if (bid.mediaTypes != undefined) { if (bid.mediaTypes.video != undefined) { - return true; + raiFormat = 'video'; } } - return false; + return raiFormat; } function raiGetVideoInfo(bid) { - let videoData = []; - if (raiGetDemandType(bid)) { - videoData.push({format: bid.mediaTypes.video.context}); - videoData.push({playerSize: bid.mediaTypes.video.playerSize}); - videoData.push({mimes: bid.mediaTypes.video.mimes}); + let videoData; + if (raiGetDemandType(bid) == 'video') { + videoData = { + format: bid.mediaTypes.video.context, + playerSize: bid.mediaTypes.video.playerSize, + mimes: bid.mediaTypes.video.mimes + }; } return videoData; } @@ -206,3 +220,24 @@ function raiSetUserId(bid, eids, source, value) { }); } } + +function renderer(bid) { + bid.renderer.push(() => { + renderAd(bid) + }); +} + +function renderAd(bid) { + let raOutstreamHBPassback = `${bid.vastXml}`; + let raPlayerHB = { + config: bid.params[0].player != undefined ? { + end: bid.params[0].player.end != null ? bid.params[0].player.end : 'close', + init: bid.params[0].player.init != null ? bid.params[0].player.init : 'close', + skin: bid.params[0].player.skin != null ? bid.params[0].player.skin : 'light', + } : {end: 'close', init: 'close', skin: 'light'}, + pid: bid.params[0].pid, + adUnit: bid.adUnitCode + }; + + window.raParams(raPlayerHB, raOutstreamHBPassback, true); +} diff --git a/modules/richaudienceBidAdapter.md b/modules/richaudienceBidAdapter.md index 852af5f1844..932cdb8f8de 100644 --- a/modules/richaudienceBidAdapter.md +++ b/modules/richaudienceBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Rich Audience Bidder Adapter Module Type: Bidder Adapter -Maintainer: cert@richaudience.com +Maintainer: cert@richaudience.com ``` # Description @@ -15,7 +15,7 @@ Please reach out to your account manager for more information. # Test Parameters -## Web +## Web - DISPLAY ``` var adUnits = [ { @@ -45,6 +45,33 @@ Please reach out to your account manager for more information. ]; ``` +## Web - VIDEO +``` + var adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, + + bids: [{ + bidder: 'richaudience', + params: { + pid: 'OjUW9KhuQV', + supplyType: 'site', + player: { + init: "open", + end: "close", + skin: "light" + } + } + }] + + }]; +``` + ## In-app ``` var adUnits = [ diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index 2bc699b4640..f18e67a3ac5 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -29,12 +29,12 @@ describe('Richaudience adapter tests', function () { user: {} }]; - var DEFAULT_PARAMS_VIDEO = [{ + var DEFAULT_PARAMS_VIDEO_IN = [{ adUnitCode: 'test-div', bidId: '2c7c8e9c900244', mediaTypes: { video: { - context: 'instream', // or 'outstream' + context: 'instream', playerSize: [640, 480], mimes: ['video/mp4'] } @@ -52,6 +52,57 @@ describe('Richaudience adapter tests', function () { user: {} }]; + var DEFAULT_PARAMS_VIDEO_OUT = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + mimes: ['video/mp4'] + } + }, + bidder: 'richaudience', + params: { + bidfloor: 0.5, + pid: 'ADb1f40rmi', + supplyType: 'site' + }, + auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6', + user: {} + }]; + + var DEFAULT_PARAMS_VIDEO_OUT_PARAMS = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + mimes: ['video/mp4'] + } + }, + bidder: 'richaudience', + params: { + bidfloor: 0.5, + pid: 'ADb1f40rmi', + supplyType: 'site', + player: { + init: 'close', + end: 'close', + skin: 'dark' + } + }, + auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6', + user: {} + }]; + var DEFAULT_PARAMS_APP = [{ adUnitCode: 'test-div', bidId: '2c7c8e9c900244', @@ -191,6 +242,45 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('numIframes').and.to.equal(0); }) + it('Verify build request to prebid video inestream', function() { + const request = spec.buildRequests(DEFAULT_PARAMS_VIDEO_IN, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: { + referer: 'https://domain.com', + numIframes: 0 + } + }); + + expect(request[0]).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent).to.have.property('demand').and.to.equal('video'); + expect(requestContent.videoData).to.have.property('format').and.to.equal('instream'); + // expect(requestContent.videoData.playerSize[0][0]).to.equal('640'); + // expect(requestContent.videoData.playerSize[0][0]).to.equal('480'); + }) + + it('Verify build request to prebid video outstream', function() { + const request = spec.buildRequests(DEFAULT_PARAMS_VIDEO_OUT, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: { + referer: 'https://domain.com', + numIframes: 0 + } + }); + + expect(request[0]).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent.videoData).to.have.property('format').and.to.equal('outstream'); + }) + describe('gdpr test', function () { it('Verify build request with GDPR', function () { config.setConfig({ @@ -502,8 +592,26 @@ describe('Richaudience adapter tests', function () { expect(bid.dealId).to.equal('dealId'); }); - it('no banner media response', function () { - const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES, { + it('no banner media response inestream', function () { + const request = spec.buildRequests(DEFAULT_PARAMS_VIDEO_IN, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: { + referer: 'https://domain.com', + numIframes: 0 + } + }); + + const bids = spec.interpretResponse(BID_RESPONSE_VIDEO, request[0]); + const bid = bids[0]; + expect(bid.mediaType).to.equal('video'); + expect(bid.vastXml).to.equal(''); + }); + + it('no banner media response outstream', function () { + const request = spec.buildRequests(DEFAULT_PARAMS_VIDEO_OUT, { gdprConsent: { consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', gdprApplies: true @@ -516,7 +624,9 @@ describe('Richaudience adapter tests', function () { const bids = spec.interpretResponse(BID_RESPONSE_VIDEO, request[0]); const bid = bids[0]; + expect(bid.mediaType).to.equal('video'); expect(bid.vastXml).to.equal(''); + expect(bid.renderer.url).to.equal('https://cdn3.richaudience.com/prebidVideo/player.js'); }); it('Verifies bidder_code', function () { From bcfc972986d5590dcc5215c48c242f1fbc665e8f Mon Sep 17 00:00:00 2001 From: matthieularere-msq <63732822+matthieularere-msq@users.noreply.github.com> Date: Tue, 30 Jun 2020 04:42:40 +0200 Subject: [PATCH 12/39] =?UTF-8?q?Mediasquare:=20Add=20support=20for=20uspC?= =?UTF-8?q?onsent=20+=20schain=20userIds=20support.=20Plu=E2=80=A6=20(#539?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Mediasquare: Add support for uspConsent + schain userIds support. Plus enhance userSync * fix iframeEnabled and pixelEnabled + suggested shortand statement --- modules/mediasquareBidAdapter.js | 36 ++++++++-------- .../modules/mediasquareBidAdapter_spec.js | 42 +++++++++++-------- 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 91adfacb64d..cb52c288caf 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -21,7 +21,7 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function(bid) { - return !!(bid.params.owner || bid.params.code); + return !!(bid.params.owner && bid.params.code); }, /** * Make a server request from the list of BidRequests. @@ -49,13 +49,17 @@ export const spec = { const payload = { codes: codes, referer: encodeURIComponent(bidderRequest.refererInfo.referer) - // schain: validBidRequests.schain, }; - if (bidderRequest && bidderRequest.gdprConsent) { - payload.gdpr = { - consent_string: bidderRequest.gdprConsent.consentString, - consent_required: bidderRequest.gdprConsent.gdprApplies - }; + if (bidderRequest) { // modules informations (gdpr, ccpa, schain, userId) + if (bidderRequest.gdprConsent) { + payload.gdpr = { + consent_string: bidderRequest.gdprConsent.consentString, + consent_required: bidderRequest.gdprConsent.gdprApplies + }; + } + if (bidderRequest.uspConsent) { payload.uspConsent = bidderRequest.uspConsent; } + if (bidderRequest.schain) { payload.schain = bidderRequest.schain; } + if (bidderRequest.userId) { payload.userId = bidderRequest.userId; } }; if (test) { payload.debug = true; } const payloadString = JSON.stringify(payload); @@ -109,25 +113,19 @@ export const spec = { * @param {ServerResponse[]} serverResponses List of server's responses. * @return {UserSync[]} The user syncs which should be dropped. */ - getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { let params = ''; let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD; - if (gdprConsent && typeof gdprConsent.consentString === 'string') { - if (typeof gdprConsent.gdprApplies === 'boolean') { params += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; } else { params += `&gdpr_consent=${gdprConsent.consentString}`; } - } - if (syncOptions.iframeEnabled) { + if (serverResponses[0].body.hasOwnProperty('cookies') && typeof serverResponses[0].body.cookies === 'object') { + return serverResponses[0].body.cookies; + } else { + if (gdprConsent && typeof gdprConsent.consentString === 'string') { params += typeof gdprConsent.gdprApplies === 'boolean' ? `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}` : `&gdpr_consent=${gdprConsent.consentString}`; } + if (uspConsent && typeof uspConsent === 'string') { params += '&uspConsent=' + uspConsent } return { type: 'iframe', url: endpoint + BIDDER_ENDPOINT_SYNC + '?type=iframe' + params }; } - if (syncOptions.pixelEnabled) { - return { - type: 'image', - url: endpoint + BIDDER_ENDPOINT_SYNC + '?type=pixel' + params - }; - } - return false; }, /** diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index 56c1fd105fe..351fbb40228 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -39,7 +39,7 @@ describe('MediaSquare bid adapter tests', function () { 'bidder': 'msqClassic', 'code': 'test/publishername_atf_desktop_rg_pave', 'bid_id': 'aaaa1234', - }] + }], }}; const DEFAULT_OPTIONS = { @@ -51,7 +51,21 @@ describe('MediaSquare bid adapter tests', function () { refererInfo: { referer: 'https://www.prebid.org', canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' - } + }, + uspConsent: '111222333', + userId: {'id5id': '1111'}, + schain: { + 'ver': '1.0', + 'complete': 1, + 'nodes': [{ + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + }] + }, }; it('Verify build request', function () { const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); @@ -103,21 +117,15 @@ describe('MediaSquare bid adapter tests', function () { const won = spec.onBidWon(response[0]); expect(won).to.equal(true); }); - it('Verifies user sync', function () { - var syncs = spec.getUserSyncs({ - iframeEnabled: true, - pixelEnabled: false, - }, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent); + it('Verifies user sync without cookie in bid response', function () { + var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); expect(syncs).to.have.property('type').and.to.equal('iframe'); - syncs = spec.getUserSyncs({ - iframeEnabled: false, - pixelEnabled: true, - }, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent); - expect(syncs).to.have.property('type').and.to.equal('image'); - syncs = spec.getUserSyncs({ - iframeEnabled: false, - pixelEnabled: false, - }, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent); - expect(syncs).to.equal(false); + }); + it('Verifies user sync with cookies in bid response', function () { + BID_RESPONSE.body.cookies = [{'type': 'image', 'url': 'http://www.cookie.sync.org/'}]; + var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0]).to.have.property('type').and.to.equal('image'); + expect(syncs[0]).to.have.property('url').and.to.equal('http://www.cookie.sync.org/'); }); }); From 76d5c51ec0af20c8404465263281c7caec30704e Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 30 Jun 2020 17:05:01 +0200 Subject: [PATCH 13/39] upgrade id5IdSystem to use v2 of our fetch endpoint (#5406) - allow publishers to pass deterministic signals - add a counter to provide analytics on the number of auctions using the id5Id --- modules/id5IdSystem.js | 99 ++++++- modules/userId/userId.md | 5 +- test/spec/modules/id5IdSystem_spec.js | 383 ++++++++++++++++++++++++++ test/spec/modules/userId_spec.js | 54 +--- 4 files changed, 477 insertions(+), 64 deletions(-) create mode 100644 test/spec/modules/id5IdSystem_spec.js diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 151782791af..3449926c896 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -1,13 +1,22 @@ /** * This module adds ID5 to the User ID module * The {@link module:modules/userId} module is required - * @module modules/unifiedIdSystem + * @module modules/id5IdSystem * @requires module:modules/userId */ import * as utils from '../src/utils.js' -import {ajax} from '../src/ajax.js'; -import {submodule} from '../src/hook.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'; + +const MODULE_NAME = 'id5Id'; +const GVLID = 131; +const BASE_NB_COOKIE_NAME = 'pbjs-id5id'; +const NB_COOKIE_EXP_DAYS = (30 * 24 * 60 * 60 * 1000); // 30 days + +const storage = getStorageManager(GVLID, MODULE_NAME); /** @type {Submodule} */ export const id5IdSubmodule = { @@ -16,11 +25,13 @@ export const id5IdSubmodule = { * @type {string} */ name: 'id5Id', + /** * Vendor id of ID5 * @type {Number} */ - gvlid: 131, + gvlid: GVLID, + /** * decode the stored id value for passing to bid requests * @function decode @@ -28,25 +39,46 @@ export const id5IdSubmodule = { * @returns {(Object|undefined)} */ decode(value) { - return (value && typeof value['ID5ID'] === 'string') ? { 'id5id': value['ID5ID'] } : undefined; + if (value && typeof value.ID5ID === 'string') { + // don't lose our legacy value from cache + return { 'id5id': value.ID5ID }; + } else if (value && typeof value.universal_uid === 'string') { + return { 'id5id': value.universal_uid }; + } else { + return undefined; + } }, + /** * performs action to obtain id and return a value in the callback's response argument - * @function + * @function getId * @param {SubmoduleParams} [configParams] * @param {ConsentData} [consentData] * @param {(Object|undefined)} cacheIdObj * @returns {IdResponse|undefined} */ getId(configParams, consentData, cacheIdObj) { - if (!configParams || typeof configParams.partner !== 'number') { - utils.logError(`User ID - ID5 submodule requires partner to be defined as a number`); + if (!hasRequiredParams(configParams)) { return undefined; } const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; const gdprConsentString = hasGdpr ? consentData.consentString : ''; - const storedUserId = this.decode(cacheIdObj); - const url = `https://id5-sync.com/g/v1/${configParams.partner}.json?1puid=${storedUserId ? storedUserId.id5id : ''}&gdpr=${hasGdpr}&gdpr_consent=${gdprConsentString}`; + const url = `https://id5-sync.com/g/v2/${configParams.partner}.json?gdpr_consent=${gdprConsentString}&gdpr=${hasGdpr}`; + const referer = getRefererInfo(); + const signature = (cacheIdObj && cacheIdObj.signature) ? cacheIdObj.signature : ''; + const pubId = (cacheIdObj && cacheIdObj.ID5ID) ? cacheIdObj.ID5ID : ''; // TODO: remove when 1puid isn't needed + const data = { + 'partner': configParams.partner, + '1puid': pubId, // TODO: remove when 1puid isn't needed + 'nbPage': incrementNb(configParams), + 'o': 'pbjs', + 'pd': configParams.pd || '', + 'rf': referer.referer, + 's': signature, + 'top': referer.reachedTop ? 1 : 0, + 'u': referer.stack[0] || window.location.href, + 'v': '$prebid.version$' + }; const resp = function (callback) { const callbacks = { @@ -55,6 +87,7 @@ export const id5IdSubmodule = { if (response) { try { responseObj = JSON.parse(response); + resetNb(configParams); } catch (error) { utils.logError(error); } @@ -66,10 +99,54 @@ export const id5IdSubmodule = { callback(); } }; - ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); + ajax(url, callbacks, JSON.stringify(data), { method: 'POST', withCredentials: true }); }; return {callback: resp}; + }, + + /** + * Similar to Submodule#getId, this optional method returns response to for id that exists already. + * If IdResponse#id is defined, then it will be written to the current active storage even if it exists already. + * If IdResponse#callback is defined, then it'll called at the end of auction. + * It's permissible to return neither, one, or both fields. + * @function extendId + * @param {SubmoduleParams} configParams + * @param {Object} cacheIdObj - existing id, if any + * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. + */ + extendId(configParams, cacheIdObj) { + incrementNb(configParams); + return cacheIdObj; } }; +function hasRequiredParams(configParams) { + if (!configParams || typeof configParams.partner !== 'number') { + utils.logError(`User ID - ID5 submodule requires partner to be defined as a number`); + return false; + } + return true; +} +function nbCookieName(configParams) { + return hasRequiredParams(configParams) ? `${BASE_NB_COOKIE_NAME}-${configParams.partner}-nb` : undefined; +} +function nbCookieExpStr(expDays) { + return (new Date(Date.now() + expDays)).toUTCString(); +} +function storeNbInCookie(configParams, nb) { + storage.setCookie(nbCookieName(configParams), nb, nbCookieExpStr(NB_COOKIE_EXP_DAYS), 'Lax'); +} +function getNbFromCookie(configParams) { + const cacheNb = storage.getCookie(nbCookieName(configParams)); + return (cacheNb) ? parseInt(cacheNb) : 0; +} +function incrementNb(configParams) { + const nb = (getNbFromCookie(configParams) + 1); + storeNbInCookie(configParams, nb); + return nb; +} +function resetNb(configParams) { + storeNbInCookie(configParams, 0); +} + submodule('userId', id5IdSubmodule); diff --git a/modules/userId/userId.md b/modules/userId/userId.md index eb9e985a1d6..20b140c3900 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -25,12 +25,13 @@ pbjs.setConfig({ }, { name: "id5Id", params: { - partner: 173 //Set your real ID5 partner ID here for production, please ask for one at http://id5.io/prebid + partner: 173, //Set your real ID5 partner ID here for production, please ask for one at https://id5.io/universal-id + pd: "some-pd-string" // See https://console.id5.io/docs/public/prebid for details }, storage: { type: "cookie", name: "id5id", - expires: 5, // Expiration of cookies in days + expires: 90, // Expiration of cookies in days refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires' }, }, { diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js new file mode 100644 index 00000000000..5080cd94bd8 --- /dev/null +++ b/test/spec/modules/id5IdSystem_spec.js @@ -0,0 +1,383 @@ +import { init, requestBidsHook, setSubmoduleRegistry, coreStorage } from 'modules/userId/index.js'; +import { config } from 'src/config.js'; +import { id5IdSubmodule } from 'modules/id5IdSystem.js'; +import { server } from 'test/mocks/xhr.js'; +import events from 'src/events.js'; +import CONSTANTS from 'src/constants.json'; + +let expect = require('chai').expect; + +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_PARTNER = 173; + const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_PARTNER}.json`; + const ID5_COOKIE_NAME = 'id5idcookie'; + const ID5_NB_COOKIE_NAME = `pbjs-id5id-${ID5_PARTNER}-nb`; + const ID5_EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; + const ID5_STORED_ID = 'storedid5id'; + const ID5_STORED_SIGNATURE = '123456'; + const ID5_STORED_OBJ = { + 'universal_uid': ID5_STORED_ID, + 'signature': ID5_STORED_SIGNATURE + }; + const ID5_LEGACY_STORED_OBJ = { + 'ID5ID': ID5_STORED_ID + } + const ID5_RESPONSE_ID = 'newid5id'; + const ID5_RESPONSE_SIGNATURE = 'abcdef'; + const ID5_JSON_RESPONSE = { + 'universal_uid': ID5_RESPONSE_ID, + 'signature': ID5_RESPONSE_SIGNATURE, + 'link_type': 0 + }; + + function getId5FetchConfig(storageName = ID5_COOKIE_NAME, storageType = 'cookie') { + return { + name: ID5_MODULE_NAME, + params: { + partner: ID5_PARTNER + }, + storage: { + name: storageName, + type: storageType, + expires: 90 + } + } + } + function getId5ValueConfig(value) { + return { + name: ID5_MODULE_NAME, + value: { + id5id: value + } + } + } + function getUserSyncConfig(userIds) { + return { + userSync: { + userIds: userIds, + syncDelay: 0 + } + } + } + function getFetchCookieConfig() { + return getUserSyncConfig([getId5FetchConfig()]); + } + function getFetchLocalStorageConfig() { + return getUserSyncConfig([getId5FetchConfig(ID5_COOKIE_NAME, 'html5')]); + } + function getValueConfig(value) { + return getUserSyncConfig([getId5ValueConfig(value)]); + } + function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: {banner: {}, native: {}}, + sizes: [[300, 200], [300, 600]], + bids: [{bidder: 'sampleBidder', params: {placementId: 'banner-only-bidder'}}] + }; + } + + describe('Xhr Requests from getId()', function() { + const responseHeader = { 'Content-Type': 'application/json' }; + let callbackSpy = sinon.spy(); + + beforeEach(function() { + callbackSpy.resetHistory(); + }); + afterEach(function () { + + }); + + it('should fail if no partner is provided in the config', function() { + expect(id5IdSubmodule.getId()).to.be.eq(undefined); + }); + + it('should call the ID5 server with 1puid field for legacy storedObj format', function () { + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_LEGACY_STORED_OBJ).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.s).to.eq(''); + expect(requestBody.partner).to.eq(ID5_PARTNER); + expect(requestBody['1puid']).to.eq(ID5_STORED_ID); + + 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); + }); + + it('should call the ID5 server with signature field for new storedObj format', function () { + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).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.s).to.eq(ID5_STORED_SIGNATURE); + expect(requestBody.partner).to.eq(ID5_PARTNER); + expect(requestBody['1puid']).to.eq(''); + + 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); + }); + + it('should call the ID5 server with pd field when pd config is set', function () { + const pubData = 'b50ca08271795a8e7e4012813f23d505193d75c0f2e2bb99baa63aa822f66ed3'; + + let config = getId5FetchConfig().params; + config.pd = pubData; + + let submoduleCallback = id5IdSubmodule.getId(config, undefined, ID5_STORED_OBJ).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.s).to.eq(ID5_STORED_SIGNATURE); + expect(requestBody.pd).to.eq(pubData); + expect(requestBody['1puid']).to.eq(''); + + 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); + }); + + it('should call the ID5 server with empty pd field when pd config is not set', function () { + let config = getId5FetchConfig().params; + config.pd = undefined; + + let submoduleCallback = id5IdSubmodule.getId(config, undefined, ID5_STORED_OBJ).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.pd).to.eq(''); + + 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); + }); + + it('should call the ID5 server with nb=1 when no stored value exists', function () { + coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).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.nbPage).to.eq(1); + + 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); + + expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + }); + + it('should call the ID5 server with incremented nb when stored value exists', function () { + let expStr = (new Date(Date.now() + 25000).toUTCString()); + coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).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.nbPage).to.eq(2); + + 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); + + expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + }); + }); + + describe('Request Bids Hook', function() { + let adUnits; + + beforeEach(function() { + sinon.stub(events, 'getEvents').returns([]); + coreStorage.setCookie(ID5_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, '', ID5_EXPIRED_COOKIE_DATE); + coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + adUnits = [getAdUnitMock()]; + }); + afterEach(function() { + events.getEvents.restore(); + coreStorage.setCookie(ID5_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, '', ID5_EXPIRED_COOKIE_DATE); + coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + }); + + it('should add stored ID from cookie to bids', function (done) { + let expStr = (new Date(Date.now() + 25000).toUTCString()); + coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); + + setSubmoduleRegistry([id5IdSubmodule]); + init(config); + config.setConfig(getFetchCookieConfig()); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); + expect(bid.userId.id5id).to.equal(ID5_STORED_ID); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: ID5_SOURCE, + uids: [{ id: ID5_STORED_ID, atype: 1 }] + }); + }); + }); + done(); + }, { adUnits }); + }); + + it('should add config value ID to bids', function (done) { + setSubmoduleRegistry([id5IdSubmodule]); + init(config); + config.setConfig(getValueConfig(ID5_STORED_ID)); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); + expect(bid.userId.id5id).to.equal(ID5_STORED_ID); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: ID5_SOURCE, + uids: [{ id: ID5_STORED_ID, atype: 1 }] + }); + }); + }); + done(); + }, { adUnits }); + }); + + it('should set nb=1 in cookie when no stored value exists', function () { + let expStr = (new Date(Date.now() + 25000).toUTCString()); + coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); + coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + + setSubmoduleRegistry([id5IdSubmodule]); + init(config); + config.setConfig(getFetchCookieConfig()); + + let innerAdUnits; + requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + + expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('1'); + }); + + it('should increment nb in cookie when stored value exists', function () { + let expStr = (new Date(Date.now() + 25000).toUTCString()); + coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); + coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + + setSubmoduleRegistry([id5IdSubmodule]); + init(config); + config.setConfig(getFetchCookieConfig()); + + let innerAdUnits; + requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + + expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('2'); + }); + + it('should call ID5 servers with signature and incremented nb post auction if refresh needed', function () { + let expStr = (new Date(Date.now() + 25000).toUTCString()); + coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); + coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, (new Date(Date.now() - 50000).toUTCString()), expStr); + coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + + let id5Config = getFetchCookieConfig(); + id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; + + setSubmoduleRegistry([id5IdSubmodule]); + init(config); + config.setConfig(id5Config); + + let innerAdUnits; + requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + + expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('2'); + + expect(server.requests).to.be.empty; + events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + + let request = server.requests[0]; + 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' }; + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + + expect(coreStorage.getCookie(ID5_COOKIE_NAME)).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); + expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + }); + + it('should call ID5 servers with 1puid and nb=1 post auction if refresh needed for legacy stored object', function () { + let expStr = (new Date(Date.now() + 25000).toUTCString()); + coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_LEGACY_STORED_OBJ), expStr); + coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, (new Date(Date.now() - 50000).toUTCString()), expStr); + + let id5Config = getFetchCookieConfig(); + id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; + + setSubmoduleRegistry([id5IdSubmodule]); + init(config); + config.setConfig(id5Config); + + let innerAdUnits; + requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + + expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('1'); + + expect(server.requests).to.be.empty; + events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + + let request = server.requests[0]; + let requestBody = JSON.parse(request.requestBody); + expect(request.url).to.contain(ID5_ENDPOINT); + expect(requestBody['1puid']).to.eq(ID5_STORED_ID); + expect(requestBody.nbPage).to.eq(1); + + const responseHeader = { 'Content-Type': 'application/json' }; + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + + expect(coreStorage.getCookie(ID5_COOKIE_NAME)).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); + expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + }); + }); + + describe('Decode stored object', function() { + const decodedObject = { 'id5id': ID5_STORED_ID }; + + it('should properly decode from a stored object', function() { + expect(id5IdSubmodule.decode(ID5_STORED_OBJ)).to.deep.equal(decodedObject); + }); + it('should properly decode from a legacy stored object', function() { + expect(id5IdSubmodule.decode(ID5_LEGACY_STORED_OBJ)).to.deep.equal(decodedObject); + }); + it('should return undefined if passed a string', function() { + expect(id5IdSubmodule.decode('somestring')).to.eq(undefined); + }); + }); +}); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 8ce81ea85b0..5dda322f3c8 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -971,54 +971,6 @@ describe('User ID', function() { done(); }, {adUnits}); }); - it('test hook from id5id cookies when refresh needed', function(done) { - // simulate existing browser local storage values - coreStorage.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); - coreStorage.setCookie('id5id_last', (new Date(Date.now() - 7200 * 1000)).toUTCString(), (new Date(Date.now() + 5000).toUTCString())); - - sinon.stub(utils, 'logError'); // getId should failed with a logError as it has no partnerId - - setSubmoduleRegistry([id5IdSubmodule]); - init(config); - config.setConfig(getConfigMock(['id5Id', 'id5id', 'cookie', 10, 3600])); - - requestBidsHook(function() { - adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid).to.have.deep.nested.property('userId.id5id'); - expect(bid.userId.id5id).to.equal('testid5id'); - expect(bid.userIdAsEids[0]).to.deep.equal({ - source: 'id5-sync.com', - uids: [{id: 'testid5id', atype: 1}] - }); - }); - }); - sinon.assert.calledOnce(utils.logError); - coreStorage.setCookie('id5id', '', EXPIRED_COOKIE_DATE); - utils.logError.restore(); - done(); - }, {adUnits}); - }); - - it('test hook from id5id value-based config', function(done) { - setSubmoduleRegistry([id5IdSubmodule]); - init(config); - config.setConfig(getConfigValueMock('id5Id', {'id5id': 'testid5id'})); - - requestBidsHook(function() { - adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid).to.have.deep.nested.property('userId.id5id'); - expect(bid.userId.id5id).to.equal('testid5id'); - expect(bid.userIdAsEids[0]).to.deep.equal({ - source: 'id5-sync.com', - uids: [{id: 'testid5id', atype: 1}] - }); - }); - }); - done(); - }, {adUnits}); - }); it('test hook from liveIntentId html5', function(done) { // simulate existing browser local storage values @@ -1126,7 +1078,7 @@ describe('User ID', function() { it('test hook when pubCommonId, unifiedId, id5Id, identityLink, britepoolId, netId and sharedId have data to pass', function(done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'testunifiedid'}), (new Date(Date.now() + 5000).toUTCString())); - coreStorage.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1203,7 +1155,7 @@ describe('User ID', function() { it('test hook when pubCommonId, unifiedId, id5Id, britepoolId, netId and sharedId have their modules added before and after init', function(done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); - coreStorage.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1296,7 +1248,7 @@ describe('User ID', function() { it('should add new id system ', function(done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); - coreStorage.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), new Date(Date.now() + 5000).toUTCString()); From c9bcc958c082f7ea74289b0e93c6d64b697e2550 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal <7393273+jaiminpanchal27@users.noreply.github.com> Date: Tue, 30 Jun 2020 11:24:03 -0400 Subject: [PATCH 14/39] Add tradedesk user id to appnexus adapter (#5346) * Add tradedesk id support * Updating appnexus payload for criteo Co-authored-by: Jaimin Panchal --- modules/appnexusBidAdapter.js | 22 ++++++++++--- test/spec/modules/appnexusBidAdapter_spec.js | 34 +++++++++++++------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index d853ca184ce..d489f55bd7e 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -207,14 +207,26 @@ export const spec = { }); } + let eids = []; const criteoId = utils.deepAccess(bidRequests[0], `userId.criteoId`); if (criteoId) { - let tpuids = []; - tpuids.push({ - 'provider': 'criteo', - 'user_id': criteoId + eids.push({ + source: 'criteo.com', + id: criteoId }); - payload.tpuids = tpuids; + } + + const tdid = utils.deepAccess(bidRequests[0], `userId.tdid`); + if (tdid) { + eids.push({ + source: 'adserver.org', + id: tdid, + rti_partner: 'TDID' + }); + } + + if (eids.length) { + payload.eids = eids; } if (tags[0].publisher_id) { diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index a0ed6af6b89..7e985045c45 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -738,18 +738,6 @@ describe('AppNexusAdapter', function () { }); }); - it('should populate tpids array when userId is available', function () { - const bidRequest = Object.assign({}, bidRequests[0], { - userId: { - criteoId: 'sample-userid' - } - }); - - const request = spec.buildRequests([bidRequest]); - const payload = JSON.parse(request.data); - expect(payload.tpuids).to.deep.equal([{provider: 'criteo', user_id: 'sample-userid'}]); - }); - it('should populate schain if available', function () { const bidRequest = Object.assign({}, bidRequests[0], { schain: { @@ -819,6 +807,28 @@ describe('AppNexusAdapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.options).to.deep.equal({withCredentials: false}); }); + + it('should populate eids array when ttd id and criteo is available', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + userId: { + tdid: 'sample-userid', + criteoId: 'sample-criteo-userid' + } + }); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.eids).to.deep.include({ + source: 'adserver.org', + id: 'sample-userid', + rti_partner: 'TDID' + }); + + expect(payload.eids).to.deep.include({ + source: 'criteo.com', + id: 'sample-criteo-userid', + }); + }); }) describe('interpretResponse', function () { From 4121e1aa04ff748b9bb59cbad4e3aa173dd8dddd Mon Sep 17 00:00:00 2001 From: invibes <51820283+invibes@users.noreply.github.com> Date: Wed, 1 Jul 2020 12:58:20 +0300 Subject: [PATCH 15/39] Add TCF2 Support for Invibes (#5378) * added tcf 2.0 * Updated adapter to support gdprEnforcement * reverted storage manager initialization Co-authored-by: florin_nedelcu_invibes --- modules/invibesBidAdapter.js | 65 ++- test/spec/modules/invibesBidAdapter_spec.js | 435 ++++++++++++++++++-- 2 files changed, 453 insertions(+), 47 deletions(-) diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index e839c173a93..220aed47e15 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -1,7 +1,6 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; -const storage = getStorageManager(); const CONSTANTS = { BIDDER_CODE: 'invibes', @@ -14,8 +13,11 @@ const CONSTANTS = { INVIBES_VENDOR_ID: 436 }; +const storage = getStorageManager(CONSTANTS.INVIBES_VENDOR_ID); + export const spec = { code: CONSTANTS.BIDDER_CODE, + gvlid: CONSTANTS.INVIBES_VENDOR_ID, /** * @param {object} bid * @return boolean @@ -364,13 +366,70 @@ function acceptPostMessage(e) { } function readGdprConsent(gdprConsent) { - if (gdprConsent && gdprConsent.vendorData && gdprConsent.vendorData.vendorConsents) { - return !!gdprConsent.vendorData.vendorConsents[CONSTANTS.INVIBES_VENDOR_ID.toString(10)] === true ? 2 : -2; + if (gdprConsent && gdprConsent.vendorData) { + if (!gdprConsent.vendorData.gdprApplies || gdprConsent.vendorData.hasGlobalConsent) { + return 2; + } + + let purposeConsents = getPurposeConsents(gdprConsent.vendorData); + + if (purposeConsents == null) { return 0; } + let properties = Object.keys(purposeConsents); + let purposeConsentsCounter = getPurposeConsentsCounter(gdprConsent.vendorData); + + if (properties.length < purposeConsentsCounter) { + return 0; + } + + for (let i = 0; i < purposeConsentsCounter; i++) { + if (!purposeConsents[properties[i]] || purposeConsents[properties[i]] === 'false') { return 0; } + } + + let vendorConsents = getVendorConsents(gdprConsent.vendorData); + if (vendorConsents == null || vendorConsents[CONSTANTS.INVIBES_VENDOR_ID.toString(10)] == null) { + return 4; + } + + if (vendorConsents[CONSTANTS.INVIBES_VENDOR_ID.toString(10)] === false) { return 0; } + + return 2; } return 0; } +function getPurposeConsentsCounter(vendorData) { + if (vendorData.purpose && vendorData.purpose.consents) { + return 10; + } + + return 5; +} + +function getPurposeConsents(vendorData) { + if (vendorData.purpose && vendorData.purpose.consents) { + return vendorData.purpose.consents; + } + + if (vendorData.purposeConsents) { + return vendorData.purposeConsents; + } + + return null; +} + +function getVendorConsents(vendorData) { + if (vendorData.vendor && vendorData.vendor.consents) { + return vendorData.vendor.consents; + } + + if (vendorData.vendorConsents) { + return vendorData.vendorConsents; + } + + return null; +} + const ivLogger = initLogger(); /// Local domain cookie management ===================== diff --git a/test/spec/modules/invibesBidAdapter_spec.js b/test/spec/modules/invibesBidAdapter_spec.js index d21405a8b9d..b75c6a4578f 100644 --- a/test/spec/modules/invibesBidAdapter_spec.js +++ b/test/spec/modules/invibesBidAdapter_spec.js @@ -1,5 +1,5 @@ -import { expect } from 'chai'; -import { spec, resetInvibes, stubDomainOptions } from 'modules/invibesBidAdapter.js'; +import {expect} from 'chai'; +import {spec, resetInvibes, stubDomainOptions, readGdprConsent} from 'modules/invibesBidAdapter.js'; describe('invibesBidAdapter:', function () { const BIDDER_CODE = 'invibes'; @@ -40,14 +40,15 @@ describe('invibesBidAdapter:', function () { } ]; - let StubbedPersistence = function(initialValue) { + let StubbedPersistence = function (initialValue) { var value = initialValue; return { load: function () { let str = value || ''; try { return JSON.parse(str); - } catch (e) { } + } catch (e) { + } }, save: function (obj) { value = JSON.stringify(obj); @@ -67,7 +68,7 @@ describe('invibesBidAdapter:', function () { describe('isBidRequestValid:', function () { context('valid bid request:', function () { - it('returns true when bidder params.placementId is set', function() { + it('returns true when bidder params.placementId is set', function () { const validBid = { bidder: BIDDER_CODE, params: { @@ -88,7 +89,7 @@ describe('invibesBidAdapter:', function () { expect(spec.isBidRequestValid(invalidBid)).to.be.false; }); - it('returns false when placementId is not set', function() { + it('returns false when placementId is not set', function () { const invalidBid = { bidder: BIDDER_CODE, params: { @@ -99,7 +100,7 @@ describe('invibesBidAdapter:', function () { expect(spec.isBidRequestValid(invalidBid)).to.be.false; }); - it('returns false when bid response was previously received', function() { + it('returns false when bid response was previously received', function () { const validBid = { bidder: BIDDER_CODE, params: { @@ -107,7 +108,7 @@ describe('invibesBidAdapter:', function () { } } - top.window.invibes.bidResponse = { prop: 'prop' }; + top.window.invibes.bidResponse = {prop: 'prop'}; expect(spec.isBidRequestValid(validBid)).to.be.false; }); }); @@ -126,7 +127,7 @@ describe('invibesBidAdapter:', function () { }); it('has location, html id, placement and width/height', function () { - const request = spec.buildRequests(bidRequests, { auctionStart: Date.now() }); + const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); const parsedData = request.data; expect(parsedData.location).to.exist; expect(parsedData.videoAdHtmlId).to.exist; @@ -137,37 +138,37 @@ describe('invibesBidAdapter:', function () { it('has capped ids if local storage variable is correctly formatted', function () { localStorage.ivvcap = '{"9731":[1,1768600800000]}'; - const request = spec.buildRequests(bidRequests, { auctionStart: Date.now() }); + const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); expect(request.data.capCounts).to.equal('9731=1'); }); it('does not have capped ids if local storage variable is incorrectly formatted', function () { localStorage.ivvcap = ':[1,1574334216992]}'; - const request = spec.buildRequests(bidRequests, { auctionStart: Date.now() }); + const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); expect(request.data.capCounts).to.equal(''); }); it('does not have capped ids if local storage variable is expired', function () { localStorage.ivvcap = '{"9731":[1,1574330064104]}'; - const request = spec.buildRequests(bidRequests, { auctionStart: Date.now() }); + const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); expect(request.data.capCounts).to.equal(''); }); it('sends query string params from localstorage 1', function () { - localStorage.ivbs = JSON.stringify({ bvci: 1 }); - const request = spec.buildRequests(bidRequests, { auctionStart: Date.now() }); + localStorage.ivbs = JSON.stringify({bvci: 1}); + const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); expect(request.data.bvci).to.equal(1); }); it('sends query string params from localstorage 2', function () { - localStorage.ivbs = JSON.stringify({ invibbvlog: true }); - const request = spec.buildRequests(bidRequests, { auctionStart: Date.now() }); + localStorage.ivbs = JSON.stringify({invibbvlog: true}); + const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); expect(request.data.invibbvlog).to.equal(true); }); it('does not send query string params from localstorage if unknwon', function () { - localStorage.ivbs = JSON.stringify({ someparam: true }); - const request = spec.buildRequests(bidRequests, { auctionStart: Date.now() }); + localStorage.ivbs = JSON.stringify({someparam: true}); + const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); expect(request.data.someparam).to.be.undefined; }); @@ -191,65 +192,401 @@ describe('invibesBidAdapter:', function () { it('try to graduate but not enough count - doesnt send the domain id', function () { global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":1521818537626,"hc":0}'; - let bidderRequest = { gdprConsent: { vendorData: { vendorConsents: { 436: true } } } }; + let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.lId).to.not.exist; }); it('try to graduate but not old enough - doesnt send the domain id', function () { - let bidderRequest = { gdprConsent: { vendorData: { vendorConsents: { 436: true } } } }; + let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":' + Date.now() + ',"hc":5}'; let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.lId).to.not.exist; }); it('graduate and send the domain id', function () { - let bidderRequest = { gdprConsent: { vendorData: { vendorConsents: { 436: true } } } }; - stubDomainOptions(new StubbedPersistence('{"id":"dvdjkams6nkq","cr":1521818537626,"hc":7}')); + let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; + stubDomainOptions(new StubbedPersistence('{"id":"dvdjkams6nkq","cr":1521818537626,"hc":7}')); let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.lId).to.exist; }); it('send the domain id if already graduated', function () { - let bidderRequest = { gdprConsent: { vendorData: { vendorConsents: { 436: true } } } }; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi"}')); + let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; + stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi"}')); let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.lId).to.exist; expect(top.window.invibes.dom.tempId).to.exist; }); it('send the domain id after replacing it with new format', function () { - let bidderRequest = { gdprConsent: { vendorData: { vendorConsents: { 436: true } } } }; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); + let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; + stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.lId).to.exist; expect(top.window.invibes.dom.tempId).to.exist; }); - it('dont send the domain id if consent declined', function () { - let bidderRequest = { gdprConsent: { vendorData: { vendorConsents: { 436: false } } } }; + it('dont send the domain id if consent declined on tcf v1', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendorConsents: {436: false} + } + } + }; + stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.lId).to.not.exist; + expect(top.window.invibes.dom.tempId).to.not.exist; + expect(request.data.oi).to.equal(0); + }); + + it('dont send the domain id if consent declined on tcf v2', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: false}} + } + } + }; stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.lId).to.not.exist; expect(top.window.invibes.dom.tempId).to.not.exist; + expect(request.data.oi).to.equal(0); }); it('dont send the domain id if no consent', function () { - let bidderRequest = { }; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); + let bidderRequest = {}; + stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.lId).to.not.exist; expect(top.window.invibes.dom.tempId).to.not.exist; }); it('try to init id but was already loaded on page - does not increment the id again', function () { - let bidderRequest = { gdprConsent: { vendorData: { vendorConsents: { 436: true } } } }; + let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":1521818537626,"hc":0}'; let request = spec.buildRequests(bidRequests, bidderRequest); request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.lId).to.not.exist; expect(top.window.invibes.dom.tempId).to.exist; }); + + it('should send oi = 0 when vendorData is null', function () { + let bidderRequest = { + gdprConsent: { + vendorData: null + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(0); + }); + + it('should send oi = 2 when consent was approved on tcf v2', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(2); + }); + + it('should send oi = 0 when vendor consents for invibes are false on tcf v2', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: false}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(0); + }); + + it('should send oi = 0 when purpose consents weren\'t approved on tcf v2', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: false, + 3: false, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(0); + }); + + it('should send oi = 0 when purpose consents are less then 10 on tcf v2', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: false, + 3: false, + 4: true, + 5: true + } + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(0); + }); + + it('should send oi = 4 when vendor consents are null on tcf v2', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: null}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(4); + }); + + it('should send oi = 4 when vendor consents for invibes is null on tcf v2', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: null}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(4); + }); + + it('should send oi = 4 when vendor consents for invibes is null on tcf v1', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendorConsents: {436: null}, + purposeConsents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(4); + }); + + it('should send oi = 4 when vendor consents consents are null on tcf v1', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendorConsents: null, + purposeConsents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(4); + }); + + it('should send oi = 2 when gdpr doesn\'t apply or has global consent', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: false, + hasGlobalConsent: true, + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(2); + }); + + it('should send oi = 2 when consent was approved on tcf v1', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendorConsents: {436: true}, + purposeConsents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(2); + }); + + it('should send oi = 0 when purpose consents weren\'t approved on tcf v1', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendorConsents: {436: true}, + purposeConsents: { + 1: false, + 2: false, + 3: true, + 4: true, + 5: true + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(0); + }); + + it('should send oi = 0 when purpose consents are less then 5 on tcf v1', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendorConsents: {436: true}, + purposeConsents: { + 1: false, + 2: false, + 3: true + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(0); + }); + + it('should send oi = 0 when vendor consents for invibes are false on tcf v1', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendorConsents: {436: false}, + purposeConsents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(0); + }); }); describe('interpretResponse', function () { @@ -285,37 +622,47 @@ describe('invibesBidAdapter:', function () { context('when the response is not valid', function () { it('handles response with no bids requested', function () { - let emptyResult = spec.interpretResponse({ body: response }); + let emptyResult = spec.interpretResponse({body: response}); expect(emptyResult).to.be.empty; }); it('handles empty response', function () { - let emptyResult = spec.interpretResponse(null, { bidRequests }); + let emptyResult = spec.interpretResponse(null, {bidRequests}); expect(emptyResult).to.be.empty; }); it('handles response with bidding is not configured', function () { - let emptyResult = spec.interpretResponse({ body: { Ads: [{ BidPrice: 1 }] } }, { bidRequests }); + let emptyResult = spec.interpretResponse({body: {Ads: [{BidPrice: 1}]}}, {bidRequests}); expect(emptyResult).to.be.empty; }); it('handles response with no ads are received', function () { - let emptyResult = spec.interpretResponse({ body: { BidModel: { PlacementId: '12345' }, AdReason: 'No ads' } }, { bidRequests }); + let emptyResult = spec.interpretResponse({ + body: { + BidModel: {PlacementId: '12345'}, + AdReason: 'No ads' + } + }, {bidRequests}); expect(emptyResult).to.be.empty; }); it('handles response with no ads are received - no ad reason', function () { - let emptyResult = spec.interpretResponse({ body: { BidModel: { PlacementId: '12345' } } }, { bidRequests }); + let emptyResult = spec.interpretResponse({body: {BidModel: {PlacementId: '12345'}}}, {bidRequests}); expect(emptyResult).to.be.empty; }); it('handles response when no placement Id matches', function () { - let emptyResult = spec.interpretResponse({ body: { BidModel: { PlacementId: '123456' }, Ads: [{ BidPrice: 1 }] } }, { bidRequests }); + let emptyResult = spec.interpretResponse({ + body: { + BidModel: {PlacementId: '123456'}, + Ads: [{BidPrice: 1}] + } + }, {bidRequests}); expect(emptyResult).to.be.empty; }); it('handles response when placement Id is not present', function () { - let emptyResult = spec.interpretResponse({ BidModel: { }, Ads: [{ BidPrice: 1 }] }, { bidRequests }); + let emptyResult = spec.interpretResponse({BidModel: {}, Ads: [{BidPrice: 1}]}, {bidRequests}); expect(emptyResult).to.be.empty; }); }); @@ -324,20 +671,20 @@ describe('invibesBidAdapter:', function () { it('responds with a valid bid', function () { top.window.invibes.setCookie('a', 'b', 370); top.window.invibes.setCookie('c', 'd', 0); - let result = spec.interpretResponse({ body: response }, { bidRequests }); + let result = spec.interpretResponse({body: response}, {bidRequests}); expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); it('responds with a valid bid and uses logger', function () { localStorage.InvibesDEBUG = true; - let result = spec.interpretResponse({ body: response }, { bidRequests }); + let result = spec.interpretResponse({body: response}, {bidRequests}); expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); it('does not make multiple bids', function () { localStorage.InvibesDEBUG = false; - let result = spec.interpretResponse({ body: response }, { bidRequests }); - let secondResult = spec.interpretResponse({ body: response }, { bidRequests }); + let result = spec.interpretResponse({body: response}, {bidRequests}); + let secondResult = spec.interpretResponse({body: response}, {bidRequests}); expect(secondResult).to.be.empty; }); }); @@ -353,7 +700,7 @@ describe('invibesBidAdapter:', function () { it('returns an iframe with params if enabled', function () { top.window.invibes.optIn = 1; global.document.cookie = 'ivvbks=17639.0,1,2'; - let response = spec.getUserSyncs({ iframeEnabled: true }); + let response = spec.getUserSyncs({iframeEnabled: true}); expect(response.type).to.equal('iframe'); expect(response.url).to.include(SYNC_ENDPOINT); expect(response.url).to.include('optIn'); @@ -362,7 +709,7 @@ describe('invibesBidAdapter:', function () { }); it('returns undefined if iframe not enabled ', function () { - let response = spec.getUserSyncs({ iframeEnabled: false }); + let response = spec.getUserSyncs({iframeEnabled: false}); expect(response).to.equal(undefined); }); }); From d8e5796827a46455185292e4a498628ecdb09bc6 Mon Sep 17 00:00:00 2001 From: Nick Jacob Date: Wed, 1 Jul 2020 17:06:56 -0400 Subject: [PATCH 16/39] add AMX adapter (#5383) --- modules/amxBidAdapter.js | 279 +++++++++++++++++ modules/amxBidAdapter.md | 37 +++ test/spec/modules/amxBidAdapter_spec.js | 390 ++++++++++++++++++++++++ 3 files changed, 706 insertions(+) create mode 100644 modules/amxBidAdapter.js create mode 100644 modules/amxBidAdapter.md create mode 100644 test/spec/modules/amxBidAdapter_spec.js diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js new file mode 100644 index 00000000000..8d7e9682325 --- /dev/null +++ b/modules/amxBidAdapter.js @@ -0,0 +1,279 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { parseUrl, deepAccess, _each, formatQS, getUniqueIdentifierStr, triggerPixel } from '../src/utils.js'; + +const BIDDER_CODE = 'amx'; +const SIMPLE_TLD_TEST = /\.co\.\w{2,4}$/; +const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; +const VERSION = 'pba1.0'; +const xmlDTDRxp = /^\s*<\?xml[^\?]+\?>/; +const VAST_RXP = /^\s*<\??(?:vast|xml)/i; +const TRACKING_ENDPOINT = 'https://1x1.a-mo.net/hbx/'; + +const getLocation = (request) => + parseUrl(deepAccess(request, 'refererInfo.canonicalUrl', location.href)) + +const largestSize = (sizes, mediaTypes) => { + const allSizes = sizes + .concat(deepAccess(mediaTypes, `${BANNER}.sizes`, []) || []) + .concat(deepAccess(mediaTypes, `${VIDEO}.sizes`, []) || []) + + return allSizes.sort((a, b) => (b[0] * b[1]) - (a[0] * a[1]))[0]; +} + +const generateDTD = (xmlDocument) => + ``; + +const isVideoADM = (html) => html != null && VAST_RXP.test(html); +const getMediaType = (bid) => isVideoADM(bid.adm) ? VIDEO : BANNER; +const nullOrType = (value, type) => + value == null || (typeof value) === type // eslint-disable-line valid-typeof + +function getID(loc) { + const host = loc.hostname.split('.'); + const short = host.slice( + host.length - (SIMPLE_TLD_TEST.test(loc.host) ? 3 : 2) + ).join('.'); + return btoa(short).replace(/=+$/, ''); +} + +const enc = encodeURIComponent; + +function nestedQs (qsData) { + const out = []; + Object.keys(qsData || {}).forEach((key) => { + out.push(enc(key) + '=' + enc(String(qsData[key]))); + }); + + return enc(out.join('&')); +} + +function createBidMap(bids) { + const out = {}; + for (const bid of bids) { + out[bid.bidId] = convertRequest(bid) + } + return out; +} + +const trackEvent = (eventName, data) => + triggerPixel(`${TRACKING_ENDPOINT}g_${eventName}?${formatQS({ + ...data, + ts: Date.now(), + eid: getUniqueIdentifierStr(), + })}`); + +function convertRequest(bid) { + const size = largestSize(bid.sizes, bid.mediaTypes) || [0, 0]; + const av = bid.mediaType === VIDEO || VIDEO in bid.mediaTypes; + const tid = deepAccess(bid, 'params.tagId') + + const params = { + av, + aw: size[0], + ah: size[1], + tf: 0, + }; + + if (typeof tid === 'string' && tid.length > 0) { + params.i = tid; + } + return params; +} + +function decorateADM(bid) { + const impressions = deepAccess(bid, 'ext.himp', []) + .concat(bid.nurl != null ? [bid.nurl] : []) + .filter((imp) => imp != null && imp.length > 0) + .map((src) => ``) + .join(''); + return bid.adm + impressions; +} + +function decorateVideoADM(bid) { + const doc = new DOMParser().parseFromString(bid.adm, 'text/xml'); + if (doc.querySelector('parsererror') != null) { + return null; + } + + const root = doc.querySelector('InLine,Wrapper') + if (root == null) { + return null; + } + + const pixels = [bid.nurl].concat(bid.ext.himp || []) + .filter((url) => url != null); + + _each(pixels, (pxl) => { + const imagePixel = doc.createElement('Impression'); + const cdata = doc.createCDATASection(pxl); + imagePixel.appendChild(cdata); + root.appendChild(imagePixel); + }); + + const dtdMatch = xmlDTDRxp.exec(bid.adm); + return (dtdMatch != null ? dtdMatch[0] : generateDTD(doc)) + doc.documentElement.outerHTML; +} + +function resolveSize(bid, request, bidId) { + if (bid.w != null && bid.w > 1 && bid.h != null && bid.h > 1) { + return [bid.w, bid.h]; + } + + const bidRequest = request.m[bidId]; + if (bidRequest == null) { + return [0, 0]; + } + + return [bidRequest.aw, bidRequest.ah]; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid(bid) { + return nullOrType(deepAccess(bid, 'params.endpoint', null), 'string') && + nullOrType(deepAccess(bid, 'params.tagId', null), 'string') && + nullOrType(deepAccess(bid, 'params.testMode', null), 'boolean'); + }, + + buildRequests(bidRequests, bidderRequest) { + const loc = getLocation(bidderRequest); + const tagId = deepAccess(bidRequests[0], 'params.tagId', null); + const testMode = deepAccess(bidRequests[0], 'params.testMode', 0); + + const payload = { + a: bidderRequest.auctionId, + B: 0, + b: loc.host, + tm: testMode, + V: '$prebid.version$', + i: (testMode && tagId != null) ? tagId : getID(loc), + l: {}, + f: 0.01, + cv: VERSION, + st: 'prebid', + h: screen.height, + w: screen.width, + gs: deepAccess(bidderRequest, 'gdprConsent.gdprApplies', '0'), + gc: deepAccess(bidderRequest, 'gdprConsent.consentString', ''), + u: deepAccess(bidderRequest, 'refererInfo.canonicalUrl', loc.href), + do: loc.host, + re: deepAccess(bidderRequest, 'refererInfo.referer'), + usp: bidderRequest.uspConsent || '1---', + smt: 9, + d: '', + m: createBidMap(bidRequests), + }; + + return { + data: payload, + method: 'POST', + url: deepAccess(bidRequests[0], 'params.endpoint', DEFAULT_ENDPOINT), + withCredentials: true, + }; + }, + + getUserSyncs(syncOptions, serverResponses) { + return (serverResponses || []) + .flatMap(({ body: response }) => + response != null && response.p != null ? (response.p.hreq || []) : []) + .map((syncPixel) => + ({ + type: syncPixel.indexOf('__st=iframe') !== -1 ? 'iframe' : 'image', + url: syncPixel + }) + ).filter(({ + type + }) => syncOptions.iframeEnabled || type === 'image') + }, + + interpretResponse(serverResponse, request) { + // validate the body/response + const response = serverResponse.body; + if (response == null || typeof response === 'string') { + return []; + } + + return Object.keys(response.r).flatMap((bidID) => { + const biddata = response.r[bidID]; + return biddata.flatMap((siteBid) => + siteBid.b.map((bid) => { + const mediaType = getMediaType(bid); + const ad = mediaType === BANNER ? decorateADM(bid) : decorateVideoADM(bid); + if (ad == null) { + return null; + } + + const size = resolveSize(bid, request.data, bidID); + + return ({ + requestId: bidID, + cpm: bid.price, + width: size[0], + height: size[1], + creativeId: bid.crid, + currency: 'USD', + netRevenue: true, + [mediaType === VIDEO ? 'vastXml' : 'ad']: ad, + meta: { + advertiserDomains: bid.adomain, + mediaType, + }, + ttl: mediaType === VIDEO ? 90 : 70 + }); + })).filter((possibleBid) => possibleBid != null); + }); + }, + + onSetTargeting(targetingData) { + if (targetingData == null) { + return; + } + + trackEvent('pbst', { + A: targetingData.bidder, + w: targetingData.width, + h: targetingData.height, + bid: targetingData.adId, + c1: targetingData.mediaType, + np: targetingData.cpm, + aud: targetingData.requestId, + a: targetingData.adUnitCode, + c2: nestedQs(targetingData.adserverTargeting), + }); + }, + + onTimeout(timeoutData) { + if (timeoutData == null) { + return; + } + + trackEvent('pbto', { + A: timeoutData.bidder, + bid: timeoutData.bidId, + a: timeoutData.adUnitCode, + cn: timeoutData.timeout, + aud: timeoutData.auctionId, + }); + }, + + onBidWon(bidWinData) { + if (bidWinData == null) { + return; + } + + trackEvent('pbwin', { + A: bidWinData.bidder, + w: bidWinData.width, + h: bidWinData.height, + bid: bidWinData.adId, + C: bidWinData.mediaType === BANNER ? 0 : 1, + np: bidWinData.cpm, + a: bidWinData.adUnitCode, + }); + }, +}; + +registerBidder(spec); diff --git a/modules/amxBidAdapter.md b/modules/amxBidAdapter.md new file mode 100644 index 00000000000..c06c2e7157c --- /dev/null +++ b/modules/amxBidAdapter.md @@ -0,0 +1,37 @@ +Overview +======== + +``` +Module Name: AMX Adapter +Module Type: Bidder Adapter +Maintainer: prebid.support@amxrtb.com +``` + +Description +=========== + +This module connects web publishers to AMX RTB video and display demand. + +# Bid Parameters + +| Key | Required | Example | Description | +| --- | -------- | ------- | ----------- | +| `endpoint` | **yes** | `https://prebid.a-mo.net/a/c` | The url including https:// and any path | +| `testMode` | no | `true` | this will activate test mode / 100% fill with sample ads | +| `tagId` | no | `"eh3hffb"` | can be used for more specific targeting of inventory. Your account manager will provide this ID if needed | + +# Test Parameters + +``` +var adUnits = [{ + code: 'test-div', + sizes: [[300, 250]], + bids: [{ + bidder: 'amx', + params: { + testMode: true, + endpoint: 'https://prebid.a-mo.net/a/c', + }, + }] +}] +``` diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js new file mode 100644 index 00000000000..e19de368361 --- /dev/null +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -0,0 +1,390 @@ +import * as utils from 'src/utils.js'; +import { config } from 'src/config.js'; +import { expect } from 'chai'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec } from 'modules/amxBidAdapter.js'; +import { BANNER, VIDEO } from 'src/mediaTypes'; +import { formatQS } from 'src/utils'; + +const sampleRequestId = '82c91e127a9b93e'; +const sampleDisplayAd = (additionalImpressions) => `${additionalImpressions}`; +const sampleDisplayCRID = '78827819'; +// minimal example vast +const sampleVideoAd = (addlImpression) => ` +00:00:15${addlImpression} +`.replace(/\n+/g, '') + +const embeddedTrackingPixel = `https://1x1.a-mo.net/hbx/g_impression?A=sample&B=20903`; +const sampleNurl = 'https://example.exchange/nurl'; + +const sampleBidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: utils.getUniqueIdentifierStr(), + vendorData: {} + }, + auctionId: utils.getUniqueIdentifierStr(), + uspConsent: '1YYY', + refererInfo: { + referer: 'https://www.prebid.org', + canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' + } +}; + +const sampleBidRequestBase = { + bidder: spec.code, + params: { + endpoint: 'https://httpbin.org/post', + }, + sizes: [[320, 50]], + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + adUnitCode: 'div-gpt-ad-example', + transactionId: utils.getUniqueIdentifierStr(), + bidId: sampleRequestId, + auctionId: utils.getUniqueIdentifierStr(), +}; + +const sampleBidRequestVideo = { + ...sampleBidRequestBase, + bidId: sampleRequestId + '_video', + sizes: [[300, 150]], + mediaTypes: { + [VIDEO]: { + sizes: [[360, 250]] + } + } +}; + +const sampleServerResponse = { + 'p': { + 'hreq': ['https://1x1.a-mo.net/hbx/g_sync?partner=test', 'https://1x1.a-mo.net/hbx/g_syncf?__st=iframe'] + }, + 'r': { + [sampleRequestId]: [ + { + 'b': [ + { + 'adid': '78827819', + 'adm': sampleDisplayAd(''), + 'adomain': [ + 'example.com' + ], + 'crid': sampleDisplayCRID, + 'ext': { + 'himp': [ + embeddedTrackingPixel + ], + }, + 'nurl': sampleNurl, + 'h': 600, + 'id': '2014691335735134254', + 'impid': '1', + 'price': 0.25, + 'w': 300 + }, + { + 'adid': '222976952', + 'adm': sampleVideoAd(''), + 'adomain': [ + 'example.com' + ], + 'crid': sampleDisplayCRID, + 'ext': { + 'himp': [ + embeddedTrackingPixel + ], + }, + 'nurl': sampleNurl, + 'h': 1, + 'id': '7735706981389902829', + 'impid': '1', + 'price': 0.25, + 'w': 1 + }, + ], + } + ] + }, +} + +describe('AmxBidAdapter', () => { + describe('isBidRequestValid', () => { + it('endpoint must be an optional string', () => { + expect(spec.isBidRequestValid({params: { endpoint: 1 }})).to.equal(false) + expect(spec.isBidRequestValid({params: { endpoint: 'test' }})).to.equal(true) + }); + + it('tagId is an optional string', () => { + expect(spec.isBidRequestValid({params: { tagId: 1 }})).to.equal(false) + expect(spec.isBidRequestValid({params: { tagId: 'test' }})).to.equal(true) + }); + + it('testMode is an optional boolean', () => { + expect(spec.isBidRequestValid({params: { testMode: 1 }})).to.equal(false) + expect(spec.isBidRequestValid({params: { testMode: false }})).to.equal(true) + }); + + it('none of the params are required', () => { + expect(spec.isBidRequestValid({})).to.equal(true) + }); + }) + describe('getUserSync', () => { + it('will only sync from valid server responses', () => { + const syncs = spec.getUserSyncs({ iframeEnabled: true }); + expect(syncs).to.eql([]); + }); + + it('will return valid syncs from a server response', () => { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [{body: sampleServerResponse}]); + expect(syncs.length).to.equal(2); + expect(syncs[0].type).to.equal('image'); + expect(syncs[1].type).to.equal('iframe'); + }); + + it('will filter out iframe syncs based on options', () => { + const syncs = spec.getUserSyncs({ iframeEnabled: false }, [{body: sampleServerResponse}, {body: sampleServerResponse}]); + expect(syncs.length).to.equal(2); + expect(syncs).to.satisfy((allSyncs) => allSyncs.every((sync) => sync.type === 'image')) + }); + }); + + describe('buildRequests', () => { + it('will default to prebid.a-mo.net endpoint', () => { + const { url } = spec.buildRequests([], sampleBidderRequest); + expect(url).to.equal('https://prebid.a-mo.net/a/c') + }); + + it('reads test mode from the first bid request', () => { + const { data } = spec.buildRequests([{ + ...sampleBidRequestBase, + params: { + testMode: true + } + }], sampleBidderRequest); + expect(data.tm).to.equal(true); + }); + + it('handles referer data and GDPR, USP Consent', () => { + const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); + delete data.m; // don't deal with "m" in this test + + expect(data).to.deep.equal({ + a: sampleBidderRequest.auctionId, + B: 0, + b: 'www.prebid.org', + tm: 0, + V: '$prebid.version$', + i: btoa('prebid.org').replace(/=+$/, ''), + l: {}, + f: 0.01, + cv: 'pba1.0', + st: 'prebid', + h: screen.height, + w: screen.width, + gs: sampleBidderRequest.gdprConsent.gdprApplies, + gc: sampleBidderRequest.gdprConsent.consentString, + u: sampleBidderRequest.refererInfo.canonicalUrl, + do: 'www.prebid.org', + re: sampleBidderRequest.refererInfo.referer, + usp: sampleBidderRequest.uspConsent, + smt: 9, + d: '', + }) + }); + + it('can build a banner request', () => { + const { method, url, data } = spec.buildRequests([sampleBidRequestBase, { + ...sampleBidRequestBase, + bidId: sampleRequestId + '_2', + params: { + ...sampleBidRequestBase.params, + tagId: 'example' + } + }], sampleBidderRequest) + + expect(url).to.equal(sampleBidRequestBase.params.endpoint) + expect(method).to.equal('POST'); + expect(Object.keys(data.m).length).to.equal(2); + expect(data.m[sampleRequestId]).to.deep.equal({ + av: false, + aw: 300, + ah: 250, + tf: 0 + }); + expect(data.m[sampleRequestId + '_2']).to.deep.equal({ + av: false, + aw: 300, + i: 'example', + ah: 250, + tf: 0 + }); + }); + + it('can build a video request', () => { + const { data } = spec.buildRequests([sampleBidRequestVideo], sampleBidderRequest); + expect(Object.keys(data.m).length).to.equal(1); + expect(data.m[sampleRequestId + '_video']).to.deep.equal({ + av: true, + aw: 360, + ah: 250, + tf: 0 + }); + }); + }); + + describe('interpretResponse', () => { + const baseBidResponse = { + requestId: sampleRequestId, + cpm: 0.25, + creativeId: sampleDisplayCRID, + currency: 'USD', + netRevenue: true, + meta: { + advertiserDomains: ['example.com'], + }, + }; + + const baseRequest = { + data: { + m: { + [sampleRequestId]: { + aw: 300, + ah: 250, + }, + } + } + }; + + it('will handle a nobid response', () => { + const parsed = spec.interpretResponse({ body: '' }, baseRequest) + expect(parsed).to.eql([]) + }); + + it('can parse a display ad', () => { + const parsed = spec.interpretResponse({ body: sampleServerResponse }, baseRequest) + expect(parsed.length).to.equal(2) + + // we should have display, video, display + expect(parsed[0]).to.deep.equal({ + ...baseBidResponse, + meta: { + ...baseBidResponse.meta, + mediaType: BANNER, + }, + width: 300, + height: 600, // from the bid itself + ttl: 70, + ad: sampleDisplayAd( + `` + + `` + ), + }); + }); + + it('can parse a video ad', () => { + const parsed = spec.interpretResponse({ body: sampleServerResponse }, baseRequest) + expect(parsed.length).to.equal(2) + + // we should have display, video, display + expect(parsed[1]).to.deep.equal({ + ...baseBidResponse, + meta: { + ...baseBidResponse.meta, + mediaType: VIDEO, + }, + width: 300, + height: 250, + ttl: 90, + vastXml: sampleVideoAd( + `` + + `` + ), + }); + }); + }); + + describe('analytics methods', () => { + let firedPixels = []; + let _Image = window.Image; + before(() => { + _Image = window.Image; + window.Image = class FakeImage { + set src(value) { + firedPixels.push(value) + } + } + }); + + beforeEach(() => { + firedPixels = []; + }); + + after(() => { + window.Image = _Image; + }); + + it('will fire an event for onSetTargeting', () => { + spec.onSetTargeting({ + bidder: 'example', + width: 300, + height: 250, + adId: 'ad-id', + mediaType: BANNER, + cpm: 1.23, + requestId: utils.getUniqueIdentifierStr(), + adUnitCode: 'div-gpt-ad', + adserverTargeting: { + hb_pb: '1.23', + hb_adid: 'ad-id', + hb_bidder: 'example' + } + }); + expect(firedPixels.length).to.equal(1) + expect(firedPixels[0]).to.match(/\/hbx\/g_pbst/) + const parsed = new URL(firedPixels[0]); + const nestedData = parsed.searchParams.get('c2'); + expect(nestedData).to.equal(formatQS({ + hb_pb: '1.23', + hb_adid: 'ad-id', + hb_bidder: 'example' + })); + }); + + it('will log an event for timeout', () => { + spec.onTimeout({ + bidder: 'example', + bidId: 'test-bid-id', + adUnitCode: 'div-gpt-ad', + timeout: 300, + auctionId: utils.getUniqueIdentifierStr() + }); + expect(firedPixels.length).to.equal(1) + expect(firedPixels[0]).to.match(/\/hbx\/g_pbto/) + }); + + it('will log an event for prebid win', () => { + spec.onBidWon({ + bidder: 'example', + adId: 'test-ad-id', + width: 300, + height: 250, + mediaType: VIDEO, + cpm: 1.34, + adUnitCode: 'div-gpt-ad', + timeout: 300, + auctionId: utils.getUniqueIdentifierStr() + }); + expect(firedPixels.length).to.equal(1) + expect(firedPixels[0]).to.match(/\/hbx\/g_pbwin/) + + const pixel = firedPixels[0]; + const url = new URL(pixel); + expect(url.searchParams.get('C')).to.equal('1') + expect(url.searchParams.get('np')).to.equal('1.34') + }); + }); +}); From 8c87a9e90b0d810c81b5d4f5adf434be9b439003 Mon Sep 17 00:00:00 2001 From: hendrikiseke1979 <53309111+hendrikiseke1979@users.noreply.github.com> Date: Wed, 1 Jul 2020 23:29:01 +0200 Subject: [PATCH 17/39] remove onBidWon callback from adapter (#5414) * orbidder adapter: add withCredentials:true header to BidRequest and onBidWon Requests * add blank in order to trigger build again * remove blank to trigger build ... again * adding extra line to trigger build ... again * add prebid version to request * add unit test for version parameter * add version parameter to win requests * fix comment * trigger rebuild * trigger rebuild * remove onBidWon callback from adapter Co-authored-by: Volk, Rainer Co-authored-by: RainerVolk4014 <53347752+RainerVolk4014@users.noreply.github.com> Co-authored-by: siggi-otto <57615762+siggi-otto@users.noreply.github.com> Co-authored-by: Hendrik Iseke <39734979+hiseke@users.noreply.github.com> Co-authored-by: Hendrik Iseke Co-authored-by: rvolk <> --- modules/orbidderBidAdapter.js | 19 ----------- test/spec/modules/orbidderBidAdapter_spec.js | 34 -------------------- 2 files changed, 53 deletions(-) diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index d7ce5aa859a..d14e2bebd72 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -1,5 +1,3 @@ -import {detectReferer} from '../src/refererDetection.js'; -import {ajax} from '../src/ajax.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -7,7 +5,6 @@ const storage = getStorageManager(); export const spec = { code: 'orbidder', - bidParams: {}, orbidderHost: (() => { let ret = 'https://orbidder.otto.de'; try { @@ -48,7 +45,6 @@ export const spec = { params: bidRequest.params } }; - spec.bidParams[bidRequest.bidId] = bidRequest.params; if (bidderRequest && bidderRequest.gdprConsent) { ret.data.gdprConsent = { consentString: bidderRequest.gdprConsent.consentString, @@ -76,21 +72,6 @@ export const spec = { } return bidResponses; }, - - onBidWon(bid) { - const getRefererInfo = detectReferer(window); - - bid.v = $$PREBID_GLOBAL$$.version; - bid.pageUrl = getRefererInfo().referer; - if (spec.bidParams[bid.requestId] && (typeof bid.params === 'undefined')) { - bid.params = [spec.bidParams[bid.requestId]]; - } - spec.ajaxCall(`${spec.orbidderHost}/win`, JSON.stringify(bid)); - }, - - ajaxCall(endpoint, data) { - ajax(endpoint, null, data, { withCredentials: true }); - } }; registerBidder(spec); diff --git a/test/spec/modules/orbidderBidAdapter_spec.js b/test/spec/modules/orbidderBidAdapter_spec.js index c20f11da5b5..eec6ccd19f6 100644 --- a/test/spec/modules/orbidderBidAdapter_spec.js +++ b/test/spec/modules/orbidderBidAdapter_spec.js @@ -153,40 +153,6 @@ describe('orbidderBidAdapter', () => { }); }); - describe('onCallbackHandler', () => { - let ajaxStub; - const bidObj = { - adId: 'testId', - test: 1, - pageUrl: 'www.someurl.de', - referrer: 'www.somereferrer.de', - requestId: '123req456' - }; - - spec.bidParams['123req456'] = {'accountId': '123acc456'}; - - let bidObjClone = deepClone(bidObj); - bidObjClone.v = $$PREBID_GLOBAL$$.version; - bidObjClone.pageUrl = detectReferer(window)().referer; - bidObjClone.params = [{'accountId': '123acc456'}]; - - beforeEach(() => { - ajaxStub = sinon.stub(spec, 'ajaxCall'); - }); - - afterEach(() => { - ajaxStub.restore(); - }); - - it('calls orbidder\'s callback endpoint', () => { - spec.onBidWon(bidObj); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0].indexOf('https://')).to.equal(0); - expect(ajaxStub.firstCall.args[0]).to.equal(`${spec.orbidderHost}/win`); - expect(ajaxStub.firstCall.args[1]).to.equal(JSON.stringify(bidObjClone)); - }); - }); - describe('interpretResponse', () => { it('should get correct bid response', () => { const serverResponse = [ From af604da31fb9350f48576cbf2b5f77232a6ba932 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Thu, 2 Jul 2020 19:23:06 -0400 Subject: [PATCH 18/39] Make default s2s ttl configurable (#5419) * make default s2s ttl configurable --- modules/prebidServerBidAdapter/index.js | 4 ++- .../modules/prebidServerBidAdapter_spec.js | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 96986ed185c..7536851f5e1 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -71,6 +71,7 @@ config.setDefaults({ * @property {string} endpoint endpoint to contact * === optional params below === * @property {number} [timeout] timeout for S2S bidders - should be lower than `pbjs.requestBids({timeout})` + * @property {number} [defaultTtl] ttl for S2S bidders when pbs does not return a ttl on the response - defaults to 60` * @property {boolean} [cacheMarkup] whether to cache the adm result * @property {string} [adapter] adapter code to use for S2S * @property {string} [syncEndpoint] endpoint URL for syncing cookies @@ -832,7 +833,8 @@ const OPEN_RTB_PROTOCOL = { bidObject.currency = (response.cur) ? response.cur : DEFAULT_S2S_CURRENCY; // TODO: Remove when prebid-server returns ttl and netRevenue - bidObject.ttl = (bid.ttl) ? bid.ttl : DEFAULT_S2S_TTL; + const configTtl = _s2sConfig.defaultTtl || DEFAULT_S2S_TTL; + bidObject.ttl = (bid.ttl) ? bid.ttl : configTtl; bidObject.netRevenue = (bid.netRevenue) ? bid.netRevenue : DEFAULT_S2S_NETREVENUE; bids.push({ adUnit: bid.impid, bid: bidObject }); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index b4544a2ec48..d99ec9d421d 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1702,6 +1702,24 @@ describe('S2S Adapter', function () { expect(response).to.have.property('cpm', 0.5); expect(response).to.not.have.property('vastUrl'); expect(response).to.not.have.property('videoCacheKey'); + expect(response).to.have.property('ttl', 60); + }); + + it('respects defaultTtl', function () { + const s2sConfig = Object.assign({}, CONFIG, { + endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', + defaultTtl: 30 + }); + config.setConfig({ s2sConfig }); + + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB)); + + sinon.assert.calledOnce(events.emit); + const event = events.emit.firstCall.args; + sinon.assert.calledOnce(addBidResponse); + const response = addBidResponse.firstCall.args[1]; + expect(response).to.have.property('ttl', 30); }); it('handles OpenRTB video responses', function () { @@ -2155,6 +2173,15 @@ describe('S2S Adapter', function () { }) }); + it('should set default s2s ttl', function () { + config.setConfig({ + s2sConfig: { + defaultTtl: 30 + } + }); + expect(config.getConfig('s2sConfig').defaultTtl).to.deep.equal(30); + }); + it('should set syncUrlModifier', function () { config.setConfig({ s2sConfig: { From 2b4fa39ca75293fa521e5ce06de79e19c0ecf51b Mon Sep 17 00:00:00 2001 From: AaronColbyPrice <67345931+AaronColbyPrice@users.noreply.github.com> Date: Thu, 2 Jul 2020 16:48:39 -0700 Subject: [PATCH 19/39] Conversant: update prebid url (#5441) * Updating Conversant bid adapter URL to new 'cvx' * Updating Conversant bid adapter URL to new 'cvx' - updating tests to match * Updating Conversant bid adapter URL to new 'cvx': rolling back package-lock.json to avoid conflict --- modules/conversantBidAdapter.js | 2 +- test/spec/modules/conversantBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index 2ecdb2b7e98..3b3d04dc498 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -7,7 +7,7 @@ const GVLID = 24; export const storage = getStorageManager(GVLID); const BIDDER_CODE = 'conversant'; -const URL = 'https://web.hb.ad.cpe.dotomi.com/s2s/header/24'; +const URL = 'https://web.hb.ad.cpe.dotomi.com/cvx/client/hb/ortb/25'; export const spec = { code: BIDDER_CODE, diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index 7c6d6e5dd6c..d802cd288ef 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -210,7 +210,7 @@ describe('Conversant adapter tests', function() { }; const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.method).to.equal('POST'); - expect(request.url).to.equal('https://web.hb.ad.cpe.dotomi.com/s2s/header/24'); + expect(request.url).to.equal('https://web.hb.ad.cpe.dotomi.com/cvx/client/hb/ortb/25'); const payload = request.data; expect(payload).to.have.property('id', 'req000'); From 86ddf586dc655993fd04902d7e61d78da87a8c7c Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Thu, 2 Jul 2020 19:50:36 -0400 Subject: [PATCH 20/39] Update padsquad for meta.advertiserDomains (#5439) * Update padsquadBidAdapter_spec.js * Update padsquadBidAdapter.js * Update padsquadBidAdapter.js --- modules/padsquadBidAdapter.js | 1 + test/spec/modules/padsquadBidAdapter_spec.js | 1 + 2 files changed, 2 insertions(+) diff --git a/modules/padsquadBidAdapter.js b/modules/padsquadBidAdapter.js index 088cac265b9..24b1d5be3be 100644 --- a/modules/padsquadBidAdapter.js +++ b/modules/padsquadBidAdapter.js @@ -83,6 +83,7 @@ export const spec = { ad: bid.adm, ttl: DEFAULT_BID_TTL, creativeId: bid.crid, + meta: { advertiserDomains: bid.adomain }, netRevenue: DEFAULT_NET_REVENUE, currency: DEFAULT_CURRENCY, }) diff --git a/test/spec/modules/padsquadBidAdapter_spec.js b/test/spec/modules/padsquadBidAdapter_spec.js index d30b1f34a9e..7d0858ed25e 100644 --- a/test/spec/modules/padsquadBidAdapter_spec.js +++ b/test/spec/modules/padsquadBidAdapter_spec.js @@ -212,6 +212,7 @@ describe('Padsquad bid adapter', function () { expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); + expect(bids[index].meta.advertiserDomains).to.deep.equal(RESPONSE.body.seatbid[0].bid[index].adomain); expect(bids[index]).to.have.property('ttl', 30); expect(bids[index]).to.have.property('netRevenue', true); } From 2bb347f6522f475b7c4fda7adbd7d437396efae1 Mon Sep 17 00:00:00 2001 From: mamatic <52153441+mamatic@users.noreply.github.com> Date: Fri, 3 Jul 2020 01:52:47 +0200 Subject: [PATCH 21/39] ATS-identityLinkId - add additional info logging events (#5442) --- modules/identityLinkIdSystem.js | 7 +++++-- test/spec/modules/identityLinkIdSystem_spec.js | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index 7f70b7329e7..c516c06d11a 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -46,8 +46,10 @@ export const identityLinkSubmodule = { // Check ats during callback so it has a chance to initialise. // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint if (window.ats) { + utils.logInfo('ATS exists!'); window.ats.retrieveEnvelope(function (envelope) { if (envelope) { + utils.logInfo('An envelope can be retrieved from ATS!'); callback(JSON.parse(envelope).envelope); } else { getEnvelope(url, callback); @@ -63,6 +65,7 @@ export const identityLinkSubmodule = { }; // return envelope from third party endpoint function getEnvelope(url, callback) { + utils.logInfo('A 3P retrieval is attempted!'); const callbacks = { success: response => { let responseObj; @@ -70,13 +73,13 @@ function getEnvelope(url, callback) { try { responseObj = JSON.parse(response); } catch (error) { - utils.logError(error); + utils.logInfo(error); } } callback((responseObj && responseObj.envelope) ? responseObj.envelope : ''); }, error: error => { - utils.logError(`identityLink: ID fetch encountered an error`, error); + utils.logInfo(`identityLink: ID fetch encountered an error`, error); callback(); } }; diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index e6850fc77b0..0d539d5988c 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -86,7 +86,6 @@ describe('IdentityLinkId tests', function () { responseHeader, 'Unavailable' ); - expect(logErrorStub.calledOnce).to.be.true; expect(callBackSpy.calledOnce).to.be.true; }); }); From 833da08fe24ffc67028111db718b9d396ae7e0ea Mon Sep 17 00:00:00 2001 From: mamatic <52153441+mamatic@users.noreply.github.com> Date: Fri, 3 Jul 2020 02:00:25 +0200 Subject: [PATCH 22/39] ATS-change logError to logInfo type (#5443) --- modules/userId/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/userId/index.js b/modules/userId/index.js index 88a0a636caf..1c5768edae1 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -251,7 +251,7 @@ function processSubmoduleCallbacks(submodules, cb) { // cache decoded value (this is copied to every adUnit bid) submodule.idObj = submodule.submodule.decode(idObj); } else { - utils.logError(`${MODULE_NAME}: ${submodule.submodule.name} - request id responded with an empty value`); + utils.logInfo(`${MODULE_NAME}: ${submodule.submodule.name} - request id responded with an empty value`); } done(); }); From 1c8a275b81aa685c966a9c131e8f65d7ab5a0ad4 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Fri, 3 Jul 2020 20:48:27 -0400 Subject: [PATCH 23/39] Revert "add AMX adapter (#5383)" (#5455) This reverts commit d8e5796827a46455185292e4a498628ecdb09bc6. --- modules/amxBidAdapter.js | 279 ----------------- modules/amxBidAdapter.md | 37 --- test/spec/modules/amxBidAdapter_spec.js | 390 ------------------------ 3 files changed, 706 deletions(-) delete mode 100644 modules/amxBidAdapter.js delete mode 100644 modules/amxBidAdapter.md delete mode 100644 test/spec/modules/amxBidAdapter_spec.js diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js deleted file mode 100644 index 8d7e9682325..00000000000 --- a/modules/amxBidAdapter.js +++ /dev/null @@ -1,279 +0,0 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { parseUrl, deepAccess, _each, formatQS, getUniqueIdentifierStr, triggerPixel } from '../src/utils.js'; - -const BIDDER_CODE = 'amx'; -const SIMPLE_TLD_TEST = /\.co\.\w{2,4}$/; -const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; -const VERSION = 'pba1.0'; -const xmlDTDRxp = /^\s*<\?xml[^\?]+\?>/; -const VAST_RXP = /^\s*<\??(?:vast|xml)/i; -const TRACKING_ENDPOINT = 'https://1x1.a-mo.net/hbx/'; - -const getLocation = (request) => - parseUrl(deepAccess(request, 'refererInfo.canonicalUrl', location.href)) - -const largestSize = (sizes, mediaTypes) => { - const allSizes = sizes - .concat(deepAccess(mediaTypes, `${BANNER}.sizes`, []) || []) - .concat(deepAccess(mediaTypes, `${VIDEO}.sizes`, []) || []) - - return allSizes.sort((a, b) => (b[0] * b[1]) - (a[0] * a[1]))[0]; -} - -const generateDTD = (xmlDocument) => - ``; - -const isVideoADM = (html) => html != null && VAST_RXP.test(html); -const getMediaType = (bid) => isVideoADM(bid.adm) ? VIDEO : BANNER; -const nullOrType = (value, type) => - value == null || (typeof value) === type // eslint-disable-line valid-typeof - -function getID(loc) { - const host = loc.hostname.split('.'); - const short = host.slice( - host.length - (SIMPLE_TLD_TEST.test(loc.host) ? 3 : 2) - ).join('.'); - return btoa(short).replace(/=+$/, ''); -} - -const enc = encodeURIComponent; - -function nestedQs (qsData) { - const out = []; - Object.keys(qsData || {}).forEach((key) => { - out.push(enc(key) + '=' + enc(String(qsData[key]))); - }); - - return enc(out.join('&')); -} - -function createBidMap(bids) { - const out = {}; - for (const bid of bids) { - out[bid.bidId] = convertRequest(bid) - } - return out; -} - -const trackEvent = (eventName, data) => - triggerPixel(`${TRACKING_ENDPOINT}g_${eventName}?${formatQS({ - ...data, - ts: Date.now(), - eid: getUniqueIdentifierStr(), - })}`); - -function convertRequest(bid) { - const size = largestSize(bid.sizes, bid.mediaTypes) || [0, 0]; - const av = bid.mediaType === VIDEO || VIDEO in bid.mediaTypes; - const tid = deepAccess(bid, 'params.tagId') - - const params = { - av, - aw: size[0], - ah: size[1], - tf: 0, - }; - - if (typeof tid === 'string' && tid.length > 0) { - params.i = tid; - } - return params; -} - -function decorateADM(bid) { - const impressions = deepAccess(bid, 'ext.himp', []) - .concat(bid.nurl != null ? [bid.nurl] : []) - .filter((imp) => imp != null && imp.length > 0) - .map((src) => ``) - .join(''); - return bid.adm + impressions; -} - -function decorateVideoADM(bid) { - const doc = new DOMParser().parseFromString(bid.adm, 'text/xml'); - if (doc.querySelector('parsererror') != null) { - return null; - } - - const root = doc.querySelector('InLine,Wrapper') - if (root == null) { - return null; - } - - const pixels = [bid.nurl].concat(bid.ext.himp || []) - .filter((url) => url != null); - - _each(pixels, (pxl) => { - const imagePixel = doc.createElement('Impression'); - const cdata = doc.createCDATASection(pxl); - imagePixel.appendChild(cdata); - root.appendChild(imagePixel); - }); - - const dtdMatch = xmlDTDRxp.exec(bid.adm); - return (dtdMatch != null ? dtdMatch[0] : generateDTD(doc)) + doc.documentElement.outerHTML; -} - -function resolveSize(bid, request, bidId) { - if (bid.w != null && bid.w > 1 && bid.h != null && bid.h > 1) { - return [bid.w, bid.h]; - } - - const bidRequest = request.m[bidId]; - if (bidRequest == null) { - return [0, 0]; - } - - return [bidRequest.aw, bidRequest.ah]; -} - -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO], - - isBidRequestValid(bid) { - return nullOrType(deepAccess(bid, 'params.endpoint', null), 'string') && - nullOrType(deepAccess(bid, 'params.tagId', null), 'string') && - nullOrType(deepAccess(bid, 'params.testMode', null), 'boolean'); - }, - - buildRequests(bidRequests, bidderRequest) { - const loc = getLocation(bidderRequest); - const tagId = deepAccess(bidRequests[0], 'params.tagId', null); - const testMode = deepAccess(bidRequests[0], 'params.testMode', 0); - - const payload = { - a: bidderRequest.auctionId, - B: 0, - b: loc.host, - tm: testMode, - V: '$prebid.version$', - i: (testMode && tagId != null) ? tagId : getID(loc), - l: {}, - f: 0.01, - cv: VERSION, - st: 'prebid', - h: screen.height, - w: screen.width, - gs: deepAccess(bidderRequest, 'gdprConsent.gdprApplies', '0'), - gc: deepAccess(bidderRequest, 'gdprConsent.consentString', ''), - u: deepAccess(bidderRequest, 'refererInfo.canonicalUrl', loc.href), - do: loc.host, - re: deepAccess(bidderRequest, 'refererInfo.referer'), - usp: bidderRequest.uspConsent || '1---', - smt: 9, - d: '', - m: createBidMap(bidRequests), - }; - - return { - data: payload, - method: 'POST', - url: deepAccess(bidRequests[0], 'params.endpoint', DEFAULT_ENDPOINT), - withCredentials: true, - }; - }, - - getUserSyncs(syncOptions, serverResponses) { - return (serverResponses || []) - .flatMap(({ body: response }) => - response != null && response.p != null ? (response.p.hreq || []) : []) - .map((syncPixel) => - ({ - type: syncPixel.indexOf('__st=iframe') !== -1 ? 'iframe' : 'image', - url: syncPixel - }) - ).filter(({ - type - }) => syncOptions.iframeEnabled || type === 'image') - }, - - interpretResponse(serverResponse, request) { - // validate the body/response - const response = serverResponse.body; - if (response == null || typeof response === 'string') { - return []; - } - - return Object.keys(response.r).flatMap((bidID) => { - const biddata = response.r[bidID]; - return biddata.flatMap((siteBid) => - siteBid.b.map((bid) => { - const mediaType = getMediaType(bid); - const ad = mediaType === BANNER ? decorateADM(bid) : decorateVideoADM(bid); - if (ad == null) { - return null; - } - - const size = resolveSize(bid, request.data, bidID); - - return ({ - requestId: bidID, - cpm: bid.price, - width: size[0], - height: size[1], - creativeId: bid.crid, - currency: 'USD', - netRevenue: true, - [mediaType === VIDEO ? 'vastXml' : 'ad']: ad, - meta: { - advertiserDomains: bid.adomain, - mediaType, - }, - ttl: mediaType === VIDEO ? 90 : 70 - }); - })).filter((possibleBid) => possibleBid != null); - }); - }, - - onSetTargeting(targetingData) { - if (targetingData == null) { - return; - } - - trackEvent('pbst', { - A: targetingData.bidder, - w: targetingData.width, - h: targetingData.height, - bid: targetingData.adId, - c1: targetingData.mediaType, - np: targetingData.cpm, - aud: targetingData.requestId, - a: targetingData.adUnitCode, - c2: nestedQs(targetingData.adserverTargeting), - }); - }, - - onTimeout(timeoutData) { - if (timeoutData == null) { - return; - } - - trackEvent('pbto', { - A: timeoutData.bidder, - bid: timeoutData.bidId, - a: timeoutData.adUnitCode, - cn: timeoutData.timeout, - aud: timeoutData.auctionId, - }); - }, - - onBidWon(bidWinData) { - if (bidWinData == null) { - return; - } - - trackEvent('pbwin', { - A: bidWinData.bidder, - w: bidWinData.width, - h: bidWinData.height, - bid: bidWinData.adId, - C: bidWinData.mediaType === BANNER ? 0 : 1, - np: bidWinData.cpm, - a: bidWinData.adUnitCode, - }); - }, -}; - -registerBidder(spec); diff --git a/modules/amxBidAdapter.md b/modules/amxBidAdapter.md deleted file mode 100644 index c06c2e7157c..00000000000 --- a/modules/amxBidAdapter.md +++ /dev/null @@ -1,37 +0,0 @@ -Overview -======== - -``` -Module Name: AMX Adapter -Module Type: Bidder Adapter -Maintainer: prebid.support@amxrtb.com -``` - -Description -=========== - -This module connects web publishers to AMX RTB video and display demand. - -# Bid Parameters - -| Key | Required | Example | Description | -| --- | -------- | ------- | ----------- | -| `endpoint` | **yes** | `https://prebid.a-mo.net/a/c` | The url including https:// and any path | -| `testMode` | no | `true` | this will activate test mode / 100% fill with sample ads | -| `tagId` | no | `"eh3hffb"` | can be used for more specific targeting of inventory. Your account manager will provide this ID if needed | - -# Test Parameters - -``` -var adUnits = [{ - code: 'test-div', - sizes: [[300, 250]], - bids: [{ - bidder: 'amx', - params: { - testMode: true, - endpoint: 'https://prebid.a-mo.net/a/c', - }, - }] -}] -``` diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js deleted file mode 100644 index e19de368361..00000000000 --- a/test/spec/modules/amxBidAdapter_spec.js +++ /dev/null @@ -1,390 +0,0 @@ -import * as utils from 'src/utils.js'; -import { config } from 'src/config.js'; -import { expect } from 'chai'; -import { newBidder } from 'src/adapters/bidderFactory.js'; -import { spec } from 'modules/amxBidAdapter.js'; -import { BANNER, VIDEO } from 'src/mediaTypes'; -import { formatQS } from 'src/utils'; - -const sampleRequestId = '82c91e127a9b93e'; -const sampleDisplayAd = (additionalImpressions) => `${additionalImpressions}`; -const sampleDisplayCRID = '78827819'; -// minimal example vast -const sampleVideoAd = (addlImpression) => ` -00:00:15${addlImpression} -`.replace(/\n+/g, '') - -const embeddedTrackingPixel = `https://1x1.a-mo.net/hbx/g_impression?A=sample&B=20903`; -const sampleNurl = 'https://example.exchange/nurl'; - -const sampleBidderRequest = { - gdprConsent: { - gdprApplies: true, - consentString: utils.getUniqueIdentifierStr(), - vendorData: {} - }, - auctionId: utils.getUniqueIdentifierStr(), - uspConsent: '1YYY', - refererInfo: { - referer: 'https://www.prebid.org', - canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' - } -}; - -const sampleBidRequestBase = { - bidder: spec.code, - params: { - endpoint: 'https://httpbin.org/post', - }, - sizes: [[320, 50]], - mediaTypes: { - [BANNER]: { - sizes: [[300, 250]] - } - }, - adUnitCode: 'div-gpt-ad-example', - transactionId: utils.getUniqueIdentifierStr(), - bidId: sampleRequestId, - auctionId: utils.getUniqueIdentifierStr(), -}; - -const sampleBidRequestVideo = { - ...sampleBidRequestBase, - bidId: sampleRequestId + '_video', - sizes: [[300, 150]], - mediaTypes: { - [VIDEO]: { - sizes: [[360, 250]] - } - } -}; - -const sampleServerResponse = { - 'p': { - 'hreq': ['https://1x1.a-mo.net/hbx/g_sync?partner=test', 'https://1x1.a-mo.net/hbx/g_syncf?__st=iframe'] - }, - 'r': { - [sampleRequestId]: [ - { - 'b': [ - { - 'adid': '78827819', - 'adm': sampleDisplayAd(''), - 'adomain': [ - 'example.com' - ], - 'crid': sampleDisplayCRID, - 'ext': { - 'himp': [ - embeddedTrackingPixel - ], - }, - 'nurl': sampleNurl, - 'h': 600, - 'id': '2014691335735134254', - 'impid': '1', - 'price': 0.25, - 'w': 300 - }, - { - 'adid': '222976952', - 'adm': sampleVideoAd(''), - 'adomain': [ - 'example.com' - ], - 'crid': sampleDisplayCRID, - 'ext': { - 'himp': [ - embeddedTrackingPixel - ], - }, - 'nurl': sampleNurl, - 'h': 1, - 'id': '7735706981389902829', - 'impid': '1', - 'price': 0.25, - 'w': 1 - }, - ], - } - ] - }, -} - -describe('AmxBidAdapter', () => { - describe('isBidRequestValid', () => { - it('endpoint must be an optional string', () => { - expect(spec.isBidRequestValid({params: { endpoint: 1 }})).to.equal(false) - expect(spec.isBidRequestValid({params: { endpoint: 'test' }})).to.equal(true) - }); - - it('tagId is an optional string', () => { - expect(spec.isBidRequestValid({params: { tagId: 1 }})).to.equal(false) - expect(spec.isBidRequestValid({params: { tagId: 'test' }})).to.equal(true) - }); - - it('testMode is an optional boolean', () => { - expect(spec.isBidRequestValid({params: { testMode: 1 }})).to.equal(false) - expect(spec.isBidRequestValid({params: { testMode: false }})).to.equal(true) - }); - - it('none of the params are required', () => { - expect(spec.isBidRequestValid({})).to.equal(true) - }); - }) - describe('getUserSync', () => { - it('will only sync from valid server responses', () => { - const syncs = spec.getUserSyncs({ iframeEnabled: true }); - expect(syncs).to.eql([]); - }); - - it('will return valid syncs from a server response', () => { - const syncs = spec.getUserSyncs({ iframeEnabled: true }, [{body: sampleServerResponse}]); - expect(syncs.length).to.equal(2); - expect(syncs[0].type).to.equal('image'); - expect(syncs[1].type).to.equal('iframe'); - }); - - it('will filter out iframe syncs based on options', () => { - const syncs = spec.getUserSyncs({ iframeEnabled: false }, [{body: sampleServerResponse}, {body: sampleServerResponse}]); - expect(syncs.length).to.equal(2); - expect(syncs).to.satisfy((allSyncs) => allSyncs.every((sync) => sync.type === 'image')) - }); - }); - - describe('buildRequests', () => { - it('will default to prebid.a-mo.net endpoint', () => { - const { url } = spec.buildRequests([], sampleBidderRequest); - expect(url).to.equal('https://prebid.a-mo.net/a/c') - }); - - it('reads test mode from the first bid request', () => { - const { data } = spec.buildRequests([{ - ...sampleBidRequestBase, - params: { - testMode: true - } - }], sampleBidderRequest); - expect(data.tm).to.equal(true); - }); - - it('handles referer data and GDPR, USP Consent', () => { - const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); - delete data.m; // don't deal with "m" in this test - - expect(data).to.deep.equal({ - a: sampleBidderRequest.auctionId, - B: 0, - b: 'www.prebid.org', - tm: 0, - V: '$prebid.version$', - i: btoa('prebid.org').replace(/=+$/, ''), - l: {}, - f: 0.01, - cv: 'pba1.0', - st: 'prebid', - h: screen.height, - w: screen.width, - gs: sampleBidderRequest.gdprConsent.gdprApplies, - gc: sampleBidderRequest.gdprConsent.consentString, - u: sampleBidderRequest.refererInfo.canonicalUrl, - do: 'www.prebid.org', - re: sampleBidderRequest.refererInfo.referer, - usp: sampleBidderRequest.uspConsent, - smt: 9, - d: '', - }) - }); - - it('can build a banner request', () => { - const { method, url, data } = spec.buildRequests([sampleBidRequestBase, { - ...sampleBidRequestBase, - bidId: sampleRequestId + '_2', - params: { - ...sampleBidRequestBase.params, - tagId: 'example' - } - }], sampleBidderRequest) - - expect(url).to.equal(sampleBidRequestBase.params.endpoint) - expect(method).to.equal('POST'); - expect(Object.keys(data.m).length).to.equal(2); - expect(data.m[sampleRequestId]).to.deep.equal({ - av: false, - aw: 300, - ah: 250, - tf: 0 - }); - expect(data.m[sampleRequestId + '_2']).to.deep.equal({ - av: false, - aw: 300, - i: 'example', - ah: 250, - tf: 0 - }); - }); - - it('can build a video request', () => { - const { data } = spec.buildRequests([sampleBidRequestVideo], sampleBidderRequest); - expect(Object.keys(data.m).length).to.equal(1); - expect(data.m[sampleRequestId + '_video']).to.deep.equal({ - av: true, - aw: 360, - ah: 250, - tf: 0 - }); - }); - }); - - describe('interpretResponse', () => { - const baseBidResponse = { - requestId: sampleRequestId, - cpm: 0.25, - creativeId: sampleDisplayCRID, - currency: 'USD', - netRevenue: true, - meta: { - advertiserDomains: ['example.com'], - }, - }; - - const baseRequest = { - data: { - m: { - [sampleRequestId]: { - aw: 300, - ah: 250, - }, - } - } - }; - - it('will handle a nobid response', () => { - const parsed = spec.interpretResponse({ body: '' }, baseRequest) - expect(parsed).to.eql([]) - }); - - it('can parse a display ad', () => { - const parsed = spec.interpretResponse({ body: sampleServerResponse }, baseRequest) - expect(parsed.length).to.equal(2) - - // we should have display, video, display - expect(parsed[0]).to.deep.equal({ - ...baseBidResponse, - meta: { - ...baseBidResponse.meta, - mediaType: BANNER, - }, - width: 300, - height: 600, // from the bid itself - ttl: 70, - ad: sampleDisplayAd( - `` + - `` - ), - }); - }); - - it('can parse a video ad', () => { - const parsed = spec.interpretResponse({ body: sampleServerResponse }, baseRequest) - expect(parsed.length).to.equal(2) - - // we should have display, video, display - expect(parsed[1]).to.deep.equal({ - ...baseBidResponse, - meta: { - ...baseBidResponse.meta, - mediaType: VIDEO, - }, - width: 300, - height: 250, - ttl: 90, - vastXml: sampleVideoAd( - `` + - `` - ), - }); - }); - }); - - describe('analytics methods', () => { - let firedPixels = []; - let _Image = window.Image; - before(() => { - _Image = window.Image; - window.Image = class FakeImage { - set src(value) { - firedPixels.push(value) - } - } - }); - - beforeEach(() => { - firedPixels = []; - }); - - after(() => { - window.Image = _Image; - }); - - it('will fire an event for onSetTargeting', () => { - spec.onSetTargeting({ - bidder: 'example', - width: 300, - height: 250, - adId: 'ad-id', - mediaType: BANNER, - cpm: 1.23, - requestId: utils.getUniqueIdentifierStr(), - adUnitCode: 'div-gpt-ad', - adserverTargeting: { - hb_pb: '1.23', - hb_adid: 'ad-id', - hb_bidder: 'example' - } - }); - expect(firedPixels.length).to.equal(1) - expect(firedPixels[0]).to.match(/\/hbx\/g_pbst/) - const parsed = new URL(firedPixels[0]); - const nestedData = parsed.searchParams.get('c2'); - expect(nestedData).to.equal(formatQS({ - hb_pb: '1.23', - hb_adid: 'ad-id', - hb_bidder: 'example' - })); - }); - - it('will log an event for timeout', () => { - spec.onTimeout({ - bidder: 'example', - bidId: 'test-bid-id', - adUnitCode: 'div-gpt-ad', - timeout: 300, - auctionId: utils.getUniqueIdentifierStr() - }); - expect(firedPixels.length).to.equal(1) - expect(firedPixels[0]).to.match(/\/hbx\/g_pbto/) - }); - - it('will log an event for prebid win', () => { - spec.onBidWon({ - bidder: 'example', - adId: 'test-ad-id', - width: 300, - height: 250, - mediaType: VIDEO, - cpm: 1.34, - adUnitCode: 'div-gpt-ad', - timeout: 300, - auctionId: utils.getUniqueIdentifierStr() - }); - expect(firedPixels.length).to.equal(1) - expect(firedPixels[0]).to.match(/\/hbx\/g_pbwin/) - - const pixel = firedPixels[0]; - const url = new URL(pixel); - expect(url.searchParams.get('C')).to.equal('1') - expect(url.searchParams.get('np')).to.equal('1.34') - }); - }); -}); From 76e680e0a629d2a62dc6de450d89fe1a60b13d21 Mon Sep 17 00:00:00 2001 From: Catalin Ciocov Date: Mon, 6 Jul 2020 00:47:39 +0300 Subject: [PATCH 24/39] Inskin Bid adapter small changes (#5373) * Add plr_AdSlot parameter needed by Inskin Pagescroll ad format * Send additional TCF related information to Inskin's ad server * Fixed linting issues. * Added unit tests --- modules/inskinBidAdapter.js | 129 +++++++++++++++++++++ test/spec/modules/inskinBidAdapter_spec.js | 85 ++++++++++++++ 2 files changed, 214 insertions(+) diff --git a/modules/inskinBidAdapter.js b/modules/inskinBidAdapter.js index a89a1b20219..2a55b5280db 100644 --- a/modules/inskinBidAdapter.js +++ b/modules/inskinBidAdapter.js @@ -57,6 +57,9 @@ export const spec = { parallel: true }, validBidRequests[0].params); + data.keywords = data.keywords || []; + const restrictions = []; + if (bidderRequest && bidderRequest.gdprConsent) { data.consent = { gdprVendorId: 150, @@ -64,6 +67,33 @@ export const spec = { // will check if the gdprApplies field was populated with a boolean value (ie from page config). If it's undefined, then default to true gdprConsentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : true }; + + if (bidderRequest.gdprConsent.apiVersion === 2) { + const purposes = [ + {id: 1, kw: 'nocookies'}, + {id: 2, kw: 'nocontext'}, + {id: 3, kw: 'nodmp'}, + {id: 4, kw: 'nodata'}, + {id: 7, kw: 'noclicks'}, + {id: 9, kw: 'noresearch'} + ]; + + const d = bidderRequest.gdprConsent.vendorData; + + if (d) { + if (d.purposeOneTreatment) { + data.keywords.push('cst-nodisclosure'); + restrictions.push('nodisclosure'); + } + + purposes.map(p => { + if (!checkConsent(p.id, d)) { + data.keywords.push('cst-' + p.kw); + restrictions.push(p.kw); + } + }); + } + } } validBidRequests.map(bid => { @@ -78,6 +108,11 @@ export const spec = { placement.adTypes.push(5, 9, 163, 2163, 3006); + if (restrictions.length) { + placement.properties = placement.properties || {}; + placement.properties.restrictions = restrictions; + } + if (placement.networkId && placement.siteId) { data.placements.push(placement); } @@ -153,6 +188,7 @@ export const spec = { const id = 'ism_tag_' + Math.floor((Math.random() * 10e16)); window[id] = { + plr_AdSlot: e.source.frameElement, bidId: e.data.bidId, bidPrice: bidsMap[e.data.bidId].price, serverResponse @@ -242,4 +278,97 @@ function retrieveAd(bidId, decision) { return "', + 'adid': '98493581', + 'adomain': [ + 'http://prebid.org' + ], + 'iurl': 'https://fra1-ib.adnxs.com/cr?id=98493581', + 'cid': '9325', + 'crid': '98493581', + 'cat': [ + 'IAB3-1' + ], + 'w': 300, + 'h': 600, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 555545, + 'auction_id': 6500448734132353000, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + }, + { + 'id': '677903815252395010', + 'impid': '2899ec066a91ff0', + 'price': 0.9, + 'adm': '', + 'adid': '98493580', + 'adomain': [ + 'http://prebid.org' + ], + 'iurl': 'https://fra1-ib.adnxs.com/cr?id=98493581', + 'cid': '9320', + 'crid': '98493580', + 'cat': [ + 'IAB3-1' + ], + 'w': 300, + 'h': 600, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 555540, + 'auction_id': 6500448734132353000, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + } ], + 'seat': 'appnexus' + } + ], + 'cur': 'GBP', /* NOTE - this is where cur is, not in the seatbids. */ + 'ext': { + 'responsetimemillis': { + 'appnexus': 47, + 'openx': 30 + } + }, + 'timing': { + 'start': 1536848078.089177, + 'end': 1536848078.142203, + 'TimeTaken': 0.05302619934082031 + } + }, + 'headers': {} +}; +/* +A bidder returns a bid for both sizes in an adunit + */ +var validResponse2BidsSameAdunit = { + 'body': { + 'id': 'd6198807-7a53-4141-b2db-d2cb754d68ba', + 'seatbid': [ + { + 'bid': [ + { + 'id': '677903815252395017', + 'impid': '2899ec066a91ff8', + 'price': 0.5, + 'adm': '', + 'adid': '98493581', + 'adomain': [ + 'http://prebid.org' + ], + 'iurl': 'https://fra1-ib.adnxs.com/cr?id=98493581', + 'cid': '9325', + 'crid': '98493581', + 'cat': [ + 'IAB3-1' + ], + 'w': 300, + 'h': 600, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 555545, + 'auction_id': 6500448734132353000, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + }, + { + 'id': '677903815252395010', + 'impid': '2899ec066a91ff8', + 'price': 0.9, + 'adm': '', + 'adid': '98493580', + 'adomain': [ + 'http://prebid.org' + ], + 'iurl': 'https://fra1-ib.adnxs.com/cr?id=98493581', + 'cid': '9320', + 'crid': '98493580', + 'cat': [ + 'IAB3-1' + ], + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 555540, + 'auction_id': 6500448734132353000, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + } ], + 'seat': 'ozappnexus' + } + ], + 'cur': 'GBP', /* NOTE - this is where cur is, not in the seatbids. */ + 'ext': { + 'responsetimemillis': { + 'appnexus': 47, + 'openx': 30 + } + }, + 'timing': { + 'start': 1536848078.089177, + 'end': 1536848078.142203, + 'TimeTaken': 0.05302619934082031 + } + }, + 'headers': {} +}; +/* + +SPECIAL CONSIDERATION FOR VIDEO TESTS: + +DO NOT USE _validVideoResponse directly - the interpretResponse function will modify it (adding a renderer!!!) so all +subsequent calls will already have a renderer attached!!! + +*/ +function getCleanValidVideoResponse() { + return JSON.parse(JSON.stringify(_validVideoResponse)); +} +var _validVideoResponse = { + 'body': { + 'id': 'd6198807-7a53-4141-b2db-d2cb754d68ba', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2899ec066a91ff8', + 'impid': '2899ec066a91ff8', + 'price': 31.7, + 'adm': '', + 'adomain': [ + 'sarr.properties' + ], + 'crid': 'ozone-655', + 'cat': [ + 'IAB21' + ], + 'w': 640, + 'h': 360, + 'ext': { + 'prebid': { + 'type': 'video' + } + }, + 'adId': '2899ec066a91ff8-2', + 'cpm': 31.7, + 'bidId': '2899ec066a91ff8', + 'requestId': '2899ec066a91ff8', + 'width': 640, + 'height': 360, + 'ad': '', + 'netRevenue': true, + 'creativeId': 'ozone-655', + 'currency': 'USD', + 'ttl': 300, + 'adserverTargeting': { + 'oz_ozbeeswax': 'ozbeeswax', + 'oz_ozbeeswax_pb': '31.7', + 'oz_ozbeeswax_crid': 'ozone-655', + 'oz_ozbeeswax_adv': 'sarr.properties', + 'oz_ozbeeswax_imp_id': '49d16ccc28663a8', + 'oz_ozbeeswax_adId': '49d16ccc28663a8-2', + 'oz_ozbeeswax_pb_r': '20.00', + 'oz_ozbeeswax_omp': '1', + 'oz_ozbeeswax_vid': 'outstream', + 'oz_auc_id': 'efa7fea0-7e87-4811-be86-fefb38c35fbb', + 'oz_winner': 'ozbeeswax', + 'oz_response_id': 'efa7fea0-7e87-4811-be86-fefb38c35fbb', + 'oz_winner_auc_id': '49d16ccc28663a8', + 'oz_winner_imp_id': '49d16ccc28663a8', + 'oz_pb_v': '2.4.0', + 'hb_bidder': 'ozone', + 'hb_adid': '49d16ccc28663a8-2', + 'hb_pb': '20.00', + 'hb_size': '640x360', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + 'originalCpm': 31.7, + 'originalCurrency': 'USD' + } + ], + 'seat': 'ozbeeswax' + } + ], + 'ext': { + 'responsetimemillis': { + 'beeswax': 9, + 'openx': 43, + 'ozappnexus': 31, + 'ozbeeswax': 7 + } + }, + 'timing': { + 'start': 1536848078.089177, + 'end': 1536848078.142203, + 'TimeTaken': 0.05302619934082031 + } + }, + 'headers': {} +}; + +var validBidResponse1adWith2Bidders = { + 'body': { + 'id': '91221f96-b931-4acc-8f05-c2a1186fa5ac', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'd6198807-7a53-4141-b2db-d2cb754d68ba', + 'impid': '2899ec066a91ff8', + 'price': 0.36754, + 'adm': '', + 'adid': '134928661', + 'adomain': [ + 'somecompany.com' + ], + 'iurl': 'https:\/\/ams1-ib.adnxs.com\/cr?id=134928661', + 'cid': '8825', + 'crid': '134928661', + 'cat': [ + 'IAB8-15', + 'IAB8-16', + 'IAB8-4', + 'IAB8-1', + 'IAB8-14', + 'IAB8-6', + 'IAB8-13', + 'IAB8-3', + 'IAB8-17', + 'IAB8-12', + 'IAB8-8', + 'IAB8-7', + 'IAB8-2', + 'IAB8-9', + 'IAB8', + 'IAB8-11' + ], + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 14640, + 'auction_id': 1.8369641905139e+18, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + } + ], + 'seat': 'appnexus' + }, + { + 'bid': [ + { + 'id': '75665207-a1ca-49db-ba0e-a5e9c7d26f32', + 'impid': '37fff511779365a', + 'price': 1.046, + 'adm': '
removed
', + 'adomain': [ + 'kx.com' + ], + 'crid': '13005', + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + } + } + } + ], + 'seat': 'openx' + } + ], + 'ext': { + 'responsetimemillis': { + 'appnexus': 91, + 'openx': 109, + 'ozappnexus': 46, + 'ozbeeswax': 2, + 'pangaea': 91 + } + } + }, + 'headers': {} +}; + +/* +testing 2 ads, 2 bidders, one bidder bids for both slots in one adunit + */ + +var multiRequest1 = [ + { + 'bidder': 'ozone', + 'params': { + 'publisherId': 'OZONERUP0001', + 'siteId': '4204204201', + 'placementId': '0420420421', + 'customData': [ + { + 'settings': {}, + 'targeting': { + 'sens': 'f', + 'pt1': '/uk', + 'pt2': 'uk', + 'pt3': 'network-front', + 'pt4': 'ng', + 'pt5': [ + 'uk' + ], + 'pt7': 'desktop', + 'pt8': [ + 'tfmqxwj7q', + 'penl4dfdk', + 'uayf5jmv3', + 't8nyiude5', + 'sek9ghqwi' + ], + 'pt9': '|k0xw2vqzp33kklb3j5w4|||' + } + } + ], + 'lotameData': { + 'Profile': { + 'tpid': '4e5c21fc7c181c2b1eb3a73d543a27f6', + 'pid': '3a45fd4872fa01f35c49586d8dcb7c60', + 'Audiences': { + 'Audience': [ + { + 'id': '439847', + 'abbr': 'all' + }, + { + 'id': '446197', + 'abbr': 'Arts, Culture & Literature' + }, + { + 'id': '446198', + 'abbr': 'Business' + } + ] + } + } + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'mpu', + 'transactionId': '6480bac7-31b5-4723-9145-ad8966660651', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '2d30e86db743a8', + 'bidderRequestId': '1d03a1dfc563fc', + 'auctionId': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }, + { + 'bidder': 'ozone', + 'params': { + 'publisherId': 'OZONERUP0001', + 'siteId': '4204204201', + 'placementId': '0420420421', + 'customData': [ + { + 'settings': {}, + 'targeting': { + 'sens': 'f', + 'pt1': '/uk', + 'pt2': 'uk', + 'pt3': 'network-front', + 'pt4': 'ng', + 'pt5': [ + 'uk' + ], + 'pt7': 'desktop', + 'pt8': [ + 'tfmqxwj7q', + 'penl4dfdk', + 't8nxz6qzd', + 't8nyiude5', + 'sek9ghqwi' + ], + 'pt9': '|k0xw2vqzp33kklb3j5w4|||' + } + } + ], + 'lotameData': { + 'Profile': { + 'tpid': '4e5c21fc7c181c2b1eb3a73d543a27f6', + 'pid': '3a45fd4872fa01f35c49586d8dcb7c60', + 'Audiences': { + 'Audience': [ + { + 'id': '439847', + 'abbr': 'all' + }, + { + 'id': '446197', + 'abbr': 'Arts, Culture & Literature' + }, + { + 'id': '446198', + 'abbr': 'Business' + } + ] + } + } + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 728, + 90 + ], + [ + 970, + 250 + ] + ] + } + }, + 'adUnitCode': 'leaderboard', + 'transactionId': 'a49988e6-ae7c-46c4-9598-f18db49892a0', + 'sizes': [ + [ + 728, + 90 + ], + [ + 970, + 250 + ] + ], + 'bidId': '3025f169863b7f8', + 'bidderRequestId': '1d03a1dfc563fc', + 'auctionId': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } +]; + +// WHEN sent as bidderRequest to buildRequests you should send the child: .bidderRequest +var multiBidderRequest1 = { + bidderRequest: { + 'bidderCode': 'ozone', + 'auctionId': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', + 'bidderRequestId': '1d03a1dfc563fc', + 'bids': [ + { + 'bidder': 'ozone', + 'params': { + 'publisherId': 'OZONERUP0001', + 'siteId': '4204204201', + 'placementId': '0420420421', + 'customData': [ + { + 'settings': {}, + 'targeting': { + 'sens': 'f', + 'pt1': '/uk', + 'pt2': 'uk', + 'pt3': 'network-front', + 'pt4': 'ng', + 'pt5': [ + 'uk' + ], + 'pt7': 'desktop', + 'pt8': [ + 'tfmqxwj7q', + 'txeh7uyo0', + 't8nxz6qzd', + 't8nyiude5', + 'sek9ghqwi' + ], + 'pt9': '|k0xw2vqzp33kklb3j5w4|||' + } + } + ], + 'lotameData': { + 'Profile': { + 'tpid': '4e5c21fc7c181c2b1eb3a73d543a27f6', + 'pid': '3a45fd4872fa01f35c49586d8dcb7c60', + 'Audiences': { + 'Audience': [ + { + 'id': '439847', + 'abbr': 'all' + }, + { + 'id': '446197', + 'abbr': 'Arts, Culture & Literature' + }, + { + 'id': '446198', + 'abbr': 'Business' + } + ] + } + } + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'mpu', + 'transactionId': '6480bac7-31b5-4723-9145-ad8966660651', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '2d30e86db743a8', + 'bidderRequestId': '1d03a1dfc563fc', + 'auctionId': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }, + { + 'bidder': 'ozone', + 'params': { + 'publisherId': 'OZONERUP0001', + 'siteId': '4204204201', + 'placementId': '0420420421', + 'customData': [ + { + 'settings': {}, + 'targeting': { + 'sens': 'f', + 'pt1': '/uk', + 'pt2': 'uk', + 'pt3': 'network-front', + 'pt4': 'ng', + 'pt5': [ + 'uk' + ], + 'pt7': 'desktop', + 'pt8': [ + 'tfmqxwj7q', + 'penl4dfdk', + 't8nxz6qzd', + 't8nyiude5', + 'sek9ghqwi' + ], + 'pt9': '|k0xw2vqzp33kklb3j5w4|||' + } + } + ], + 'lotameData': { + 'Profile': { + 'tpid': '4e5c21fc7c181c2b1eb3a73d543a27f6', + 'pid': '3a45fd4872fa01f35c49586d8dcb7c60', + 'Audiences': { + 'Audience': [ + { + 'id': '439847', + 'abbr': 'all' + }, + { + 'id': '446197', + 'abbr': 'Arts, Culture & Literature' + }, + { + 'id': '446198', + 'abbr': 'Business' + } + ] + } + } + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 728, + 90 + ], + [ + 970, + 250 + ] + ] + } + }, + 'adUnitCode': 'leaderboard', + 'transactionId': 'a49988e6-ae7c-46c4-9598-f18db49892a0', + 'sizes': [ + [ + 728, + 90 + ], + [ + 970, + 250 + ] + ], + 'bidId': '3025f169863b7f8', + 'bidderRequestId': '1d03a1dfc563fc', + 'auctionId': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ], + 'auctionStart': 1592918645574, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'http://ozone.ardm.io/adapter/2.4.0/620x350-switch.html?guardian=true&pbjs_debug=true', + 'reachedTop': true, + 'numIframes': 0, + 'stack': [ + 'http://ozone.ardm.io/adapter/2.4.0/620x350-switch.html?guardian=true&pbjs_debug=true' + ] + }, + 'gdprConsent': { + 'consentString': 'BOvy5sFO1dBa2AKAiBENDP-AAAAwVrv7_77-_9f-_f__9uj3Gr_v_f__32ccL5tv3h_7v-_7fi_-0nV4u_1tft9ydk1-5ctDztp507iakiPHmqNeb9n_mz1eZpRP58E09j53z7Ew_v8_v-b7BCPN_Y3v-8K96kA', + 'vendorData': { + 'metadata': 'BOvy5sFO1dBa2AKAiBENDPA', + 'gdprApplies': true, + 'hasGlobalConsent': false, + 'hasGlobalScope': false, + 'purposeConsents': { + '1': true, + '2': true, + '3': true, + '4': true, + '5': true + }, + 'vendorConsents': { + '1': true, + '2': true, + '3': false, + '4': true, + '5': true + } + }, + 'gdprApplies': true + }, + 'start': 1592918645578 + } +}; + +var multiResponse1 = { 'body': { - 'id': 'd6198807-7a53-4141-b2db-d2cb754d68ba', + 'id': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', 'seatbid': [ { 'bid': [ { - 'id': '677903815252395017', - 'impid': '2899ec066a91ff8', - 'price': 0.5, - 'adm': '', - 'adid': '98493581', + 'id': '4419718600113204943', + 'impid': '2d30e86db743a8', + 'price': 0.2484, + 'adm': '', + 'adid': '119683582', 'adomain': [ - 'http://prebid.org' + 'https://ozoneproject.com' ], - 'iurl': 'https://fra1-ib.adnxs.com/cr?id=98493581', - 'cid': '9325', - 'crid': '98493581', + 'iurl': 'https://ams1-ib.adnxs.com/cr?id=119683582', + 'cid': '9979', + 'crid': '119683582', 'cat': [ - 'IAB3-1' + 'IAB3' ], 'w': 300, - 'h': 600, + 'h': 250, 'ext': { 'prebid': { - 'type': 'video' + 'type': 'banner' }, 'bidder': { - 'unruly': { - 'renderer': { - 'config': { - 'targetingUUID': 'aafd3388-afaf-41f4-b271-0ac8e0325a7f', - 'siteId': 1052815, - 'featureOverrides': {} - }, - 'url': 'https://video.unrulymedia.com/native/native-loader.js#supplyMode=prebid?cb=6284685353877994', - 'id': 'unruly_inarticle' - }, - 'vast_url': 'data:text/xml;base64,PD94bWwgdmVyc2lvbj0i' + 'ozone': {}, + 'appnexus': { + 'brand_id': 734921, + 'auction_id': 2995348111857539600, + 'bidder_id': 2, + 'bid_ad_type': 0 } } - } - } - ], - 'seat': 'unruly' - } - ], - 'ext': { - 'responsetimemillis': { - 'appnexus': 47, - 'openx': 30 - } - }, - 'timing': { - 'start': 1536848078.089177, - 'end': 1536848078.142203, - 'TimeTaken': 0.05302619934082031 - } - }, - 'headers': {} -}; -var validBidResponse1adWith2Bidders = { - 'body': { - 'id': '91221f96-b931-4acc-8f05-c2a1186fa5ac', - 'seatbid': [ - { - 'bid': [ + }, + 'cpm': 0.2484, + 'bidId': '2d30e86db743a8', + 'requestId': '2d30e86db743a8', + 'width': 300, + 'height': 250, + 'ad': '', + 'netRevenue': true, + 'creativeId': '119683582', + 'currency': 'USD', + 'ttl': 300, + 'originalCpm': 0.2484, + 'originalCurrency': 'USD' + }, { - 'id': 'd6198807-7a53-4141-b2db-d2cb754d68ba', - 'impid': '2899ec066a91ff8', - 'price': 0.36754, - 'adm': '', - 'adid': '134928661', + 'id': '18552976939844681', + 'impid': '3025f169863b7f8', + 'price': 0.0621, + 'adm': '', + 'adid': '120179216', 'adomain': [ - 'somecompany.com' - ], - 'iurl': 'https:\/\/ams1-ib.adnxs.com\/cr?id=134928661', - 'cid': '8825', - 'crid': '134928661', - 'cat': [ - 'IAB8-15', - 'IAB8-16', - 'IAB8-4', - 'IAB8-1', - 'IAB8-14', - 'IAB8-6', - 'IAB8-13', - 'IAB8-3', - 'IAB8-17', - 'IAB8-12', - 'IAB8-8', - 'IAB8-7', - 'IAB8-2', - 'IAB8-9', - 'IAB8', - 'IAB8-11' + 'appnexus.com' ], - 'w': 300, + 'iurl': 'https://ams1-ib.adnxs.com/cr?id=120179216', + 'cid': '9979', + 'crid': '120179216', + 'w': 970, 'h': 250, 'ext': { 'prebid': { 'type': 'banner' }, 'bidder': { + 'ozone': {}, 'appnexus': { - 'brand_id': 14640, - 'auction_id': 1.8369641905139e+18, + 'brand_id': 1, + 'auction_id': 3449036134472542700, 'bidder_id': 2, 'bid_ad_type': 0 } } - } + }, + 'cpm': 0.0621, + 'bidId': '3025f169863b7f8', + 'requestId': '3025f169863b7f8', + 'width': 970, + 'height': 250, + 'ad': '', + 'netRevenue': true, + 'creativeId': '120179216', + 'currency': 'USD', + 'ttl': 300, + 'originalCpm': 0.0621, + 'originalCurrency': 'USD' + }, + { + 'id': '18552976939844999', + 'impid': '3025f169863b7f8', + 'price': 0.521, + 'adm': '', + 'adid': '120179216', + 'adomain': [ + 'appnexus.com' + ], + 'iurl': 'https://ams1-ib.adnxs.com/cr?id=120179216', + 'cid': '9999', + 'crid': '120179299', + 'w': 728, + 'h': 90, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'ozone': {}, + 'appnexus': { + 'brand_id': 1, + 'auction_id': 3449036134472542700, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + }, + 'cpm': 0.521, + 'bidId': '3025f169863b7f8', + 'requestId': '3025f169863b7f8', + 'width': 728, + 'height': 90, + 'ad': '', + 'netRevenue': true, + 'creativeId': '120179299', + 'currency': 'USD', + 'ttl': 300, + 'originalCpm': 0.0621, + 'originalCurrency': 'USD' } ], - 'seat': 'appnexus' + 'seat': 'ozappnexus' }, { 'bid': [ { - 'id': '75665207-a1ca-49db-ba0e-a5e9c7d26f32', - 'impid': '37fff511779365a', - 'price': 1.046, - 'adm': '
removed
', - 'adomain': [ - 'kx.com' - ], - 'crid': '13005', + 'id': '1c605e8a-4992-4ec6-8a5c-f82e2938c2db', + 'impid': '2d30e86db743a8', + 'price': 0.01, + 'adm': '
', + 'crid': '540463358', 'w': 300, 'h': 250, 'ext': { 'prebid': { 'type': 'banner' + }, + 'bidder': { + 'ozone': {} } - } + }, + 'cpm': 0.01, + 'bidId': '2d30e86db743a8', + 'requestId': '2d30e86db743a8', + 'width': 300, + 'height': 250, + 'ad': '
', + 'netRevenue': true, + 'creativeId': '540463358', + 'currency': 'USD', + 'ttl': 300, + 'originalCpm': 0.01, + 'originalCurrency': 'USD' + }, + { + 'id': '3edeb4f7-d91d-44e2-8aeb-4a2f6d295ce5', + 'impid': '3025f169863b7f8', + 'price': 0.01, + 'adm': '
', + 'crid': '540221061', + 'w': 970, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'ozone': {} + } + }, + 'cpm': 0.01, + 'bidId': '3025f169863b7f8', + 'requestId': '3025f169863b7f8', + 'width': 970, + 'height': 250, + 'ad': '
', + 'netRevenue': true, + 'creativeId': '540221061', + 'currency': 'USD', + 'ttl': 300, + 'originalCpm': 0.01, + 'originalCurrency': 'USD' } ], 'seat': 'openx' } ], 'ext': { + 'debug': {}, 'responsetimemillis': { - 'appnexus': 91, - 'openx': 109, - 'ozappnexus': 46, - 'ozbeeswax': 2, - 'pangaea': 91 + 'beeswax': 6, + 'openx': 91, + 'ozappnexus': 40, + 'ozbeeswax': 6 } } }, 'headers': {} }; +/* +--------------------end of 2 slots, 2 ---------------------------- + */ + describe('ozone Adapter', function () { describe('isBidRequestValid', function () { // A test ad unit that will consistently return test creatives @@ -473,7 +1589,7 @@ describe('ozone Adapter', function () { publisherId: '9876abcd12-3', siteId: '1234567890', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], - lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}], 'ThirdPartyAudience': [{'id': '123', 'name': 'Automobiles'}, {'id': '456', 'name': 'Ages: 30-39'}]}}}, + lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, }, siteId: 1234567890 } @@ -854,26 +1970,26 @@ describe('ozone Adapter', function () { describe('buildRequests', function () { it('sends bid request to OZONEURI via POST', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); expect(request.url).to.equal(OZONEURI); expect(request.method).to.equal('POST'); }); it('sends data as a string', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); expect(request.data).to.be.a('string'); }); it('sends all bid parameters', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); }); it('adds all parameters inside the ext object only', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); expect(request.data).to.be.a('string'); var data = JSON.parse(request.data); - expect(data.imp[0].ext.ozone.lotameData).to.be.an('object'); + expect(data.ext.ozone.lotameData).to.be.an('object'); expect(data.imp[0].ext.ozone.customData).to.be.an('array'); expect(request).not.to.have.key('lotameData'); expect(request).not.to.have.key('customData'); @@ -882,10 +1998,10 @@ describe('ozone Adapter', function () { it('ignores ozoneData in & after version 2.1.1', function () { let validBidRequestsWithOzoneData = validBidRequests; validBidRequestsWithOzoneData[0].params.ozoneData = {'networkID': '3048', 'dfpSiteID': 'd.thesun', 'sectionID': 'homepage', 'path': '/', 'sec_id': 'null', 'sec': 'sec', 'topics': 'null', 'kw': 'null', 'aid': 'null', 'search': 'null', 'article_type': 'null', 'hide_ads': '', 'article_slug': 'null'}; - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); expect(request.data).to.be.a('string'); var data = JSON.parse(request.data); - expect(data.imp[0].ext.ozone.lotameData).to.be.an('object'); + expect(data.ext.ozone.lotameData).to.be.an('object'); expect(data.imp[0].ext.ozone.customData).to.be.an('array'); expect(data.imp[0].ext.ozone.ozoneData).to.be.undefined; expect(request).not.to.have.key('lotameData'); @@ -893,33 +2009,33 @@ describe('ozone Adapter', function () { }); it('has correct bidder', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); expect(request.bidderRequest.bids[0].bidder).to.equal(BIDDER_CODE); }); it('handles mediaTypes element correctly', function () { - const request = spec.buildRequests(validBidRequestsWithBannerMediaType, validBidderRequest); + const request = spec.buildRequests(validBidRequestsWithBannerMediaType, validBidderRequest.bidderRequest); expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); }); it('handles no ozone, lotame or custom data', function () { - const request = spec.buildRequests(validBidRequestsMinimal, validBidderRequest); + const request = spec.buildRequests(validBidRequestsMinimal, validBidderRequest.bidderRequest); expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); }); it('handles video mediaType element correctly, with outstream video', function () { - const request = spec.buildRequests(validBidRequestsWithNonBannerMediaTypesAndValidOutstreamVideo, validBidderRequest); + const request = spec.buildRequests(validBidRequests1OutstreamVideo2020, validBidderRequest.bidderRequest); expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); }); it('should not crash when there is no sizes element at all', function () { - const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); + const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest.bidderRequest); expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); }); it('should be able to handle non-single requests', function () { config.setConfig({'ozone': {'singleRequest': false}}); - const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); + const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest.bidderRequest); expect(request).to.be.a('array'); expect(request[0]).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); // need to reset the singleRequest config flag: @@ -928,7 +2044,7 @@ describe('ozone Adapter', function () { it('should add gdpr consent information to the request when ozone is true', function () { let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; - let bidderRequest = validBidderRequest; + let bidderRequest = validBidderRequest.bidderRequest; bidderRequest.gdprConsent = { consentString: consentString, gdprApplies: true, @@ -947,9 +2063,9 @@ describe('ozone Adapter', function () { }); // mirror - it('should add gdpr consent information to the request when ozone.oz_enforceGdpr is false and vendorData is missing vendorConsents (Mirror)', function () { + it('should add gdpr consent information to the request when vendorData is missing vendorConsents (Mirror)', function () { let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; - let bidderRequest = validBidderRequest; + let bidderRequest = validBidderRequest.bidderRequest; bidderRequest.gdprConsent = { consentString: consentString, gdprApplies: true, @@ -964,43 +2080,9 @@ describe('ozone Adapter', function () { expect(payload.regs.ext.gdpr).to.equal(1); expect(payload.user.ext.consent).to.equal(consentString); }); - it('should add gdpr consent information to the request when ozone.oz_enforceGdpr is NOT PRESENT and vendorData is missing vendorConsents (Mirror)', function () { - let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; - let bidderRequest = validBidderRequest; - bidderRequest.gdprConsent = { - consentString: consentString, - gdprApplies: true, - vendorData: { - metadata: consentString, - gdprApplies: true - } - } - const request = spec.buildRequests(validBidRequestsNoSizes, bidderRequest); - const payload = JSON.parse(request.data); - expect(payload.regs.ext.gdpr).to.equal(1); - expect(payload.user.ext.consent).to.equal(consentString); - config.resetConfig(); - }); - it('should kill the auction request when ozone.oz_enforceGdpr is true & vendorData is missing vendorConsents (Mirror)', function () { - let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; - let bidderRequest = validBidderRequest; - bidderRequest.gdprConsent = { - consentString: consentString, - gdprApplies: true, - vendorData: { - metadata: consentString, - gdprApplies: true - } - } - config.setConfig({'ozone': {'oz_enforceGdpr': true}}); - const request = spec.buildRequests(validBidRequestsNoSizes, bidderRequest); - expect(request.length).to.equal(0); - config.resetConfig(); - }); - it('should set regs.ext.gdpr flag to 0 when gdprApplies is false', function () { let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; - let bidderRequest = validBidderRequest; + let bidderRequest = validBidderRequest.bidderRequest; bidderRequest.gdprConsent = { consentString: consentString, gdprApplies: false, @@ -1019,7 +2101,7 @@ describe('ozone Adapter', function () { it('should not have imp[N].ext.ozone.userId', function () { let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; - let bidderRequest = validBidderRequest; + let bidderRequest = validBidderRequest.bidderRequest; bidderRequest.gdprConsent = { consentString: consentString, gdprApplies: false, @@ -1063,14 +2145,14 @@ describe('ozone Adapter', function () { // 'pubcid': '5555', // remove pubcid from here to emulate the OLD module & cause the failover code to kick in 'tdid': '6666' }; - const request = spec.buildRequests(bidRequests, validBidderRequest); + const request = spec.buildRequests(bidRequests, validBidderRequest.bidderRequest); const payload = JSON.parse(request.data); expect(payload.ext.ozone.pubcid).to.equal(bidRequests[0]['crumbs']['pubcid']); delete validBidRequests[0].userId; // tidy up now, else it will screw with other tests }); it('should add a user.ext.eids object to contain user ID data in the new location (Nov 2019)', function() { - const request = spec.buildRequests(validBidRequestsWithUserIdData, validBidderRequest); + const request = spec.buildRequests(validBidRequestsWithUserIdData, validBidderRequest.bidderRequest); const payload = JSON.parse(request.data); expect(payload.user).to.exist; expect(payload.user.ext).to.exist; @@ -1096,7 +2178,7 @@ describe('ozone Adapter', function () { spec.getGetParametersAsObject = function() { return {'oztestmode': 'mytestvalue_123'}; }; - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const data = JSON.parse(request.data); expect(data.imp[0].ext.ozone.customData).to.be.an('array'); expect(data.imp[0].ext.ozone.customData[0].targeting.oztestmode).to.equal('mytestvalue_123'); @@ -1106,7 +2188,7 @@ describe('ozone Adapter', function () { spec.getGetParametersAsObject = function() { return {'oztestmode': 'mytestvalue_123'}; }; - const request = spec.buildRequests(validBidRequestsMinimal, validBidderRequest); + const request = spec.buildRequests(validBidRequestsMinimal, validBidderRequest.bidderRequest); const data = JSON.parse(request.data); expect(data.imp[0].ext.ozone.customData).to.be.an('array'); expect(data.imp[0].ext.ozone.customData[0].targeting.oztestmode).to.equal('mytestvalue_123'); @@ -1117,7 +2199,7 @@ describe('ozone Adapter', function () { specMock.getGetParametersAsObject = function() { return {'ozstoredrequest': '1122334455'}; // 10 digits are valid }; - const request = specMock.buildRequests(validBidRequestsMinimal, validBidderRequest); + const request = specMock.buildRequests(validBidRequestsMinimal, validBidderRequest.bidderRequest); const data = JSON.parse(request.data); expect(data.ext.ozone.oz_rw).to.equal(1); expect(data.imp[0].ext.prebid.storedrequest.id).to.equal('1122334455'); @@ -1127,7 +2209,7 @@ describe('ozone Adapter', function () { specMock.getGetParametersAsObject = function() { return {'ozstoredrequest': 'BADVAL'}; // 10 digits are valid }; - const request = specMock.buildRequests(validBidRequestsMinimal, validBidderRequest); + const request = specMock.buildRequests(validBidRequestsMinimal, validBidderRequest.bidderRequest); const data = JSON.parse(request.data); expect(data.ext.ozone.oz_rw).to.equal(0); expect(data.imp[0].ext.prebid.storedrequest.id).to.equal('1310000099'); @@ -1137,9 +2219,9 @@ describe('ozone Adapter', function () { spec.getGetParametersAsObject = function() { return {'oz_lotameid': '123abc', 'oz_lotamepid': 'pid123', 'oz_lotametpid': '123eee'}; }; - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const payload = JSON.parse(request.data); - expect(payload.imp[0].ext.ozone.lotameData.Profile.Audiences.Audience[0].id).to.equal('123abc'); + expect(payload.ext.ozone.lotameData.Profile.Audiences.Audience[0].id).to.equal('123abc'); expect(payload.ext.ozone.oz_lot_rw).to.equal(1); }); it('should pick up the value of valid lotame override parameters when there is an empty lotame object', function () { @@ -1148,11 +2230,11 @@ describe('ozone Adapter', function () { spec.getGetParametersAsObject = function() { return {'oz_lotameid': '123abc', 'oz_lotamepid': 'pid123', 'oz_lotametpid': '123eeetpid'}; }; - const request = spec.buildRequests(nolotameBidReq, validBidderRequest); + const request = spec.buildRequests(nolotameBidReq, validBidderRequest.bidderRequest); const payload = JSON.parse(request.data); - expect(payload.imp[0].ext.ozone.lotameData.Profile.Audiences.Audience[0].id).to.equal('123abc'); - expect(payload.imp[0].ext.ozone.lotameData.Profile.tpid).to.equal('123eeetpid'); - expect(payload.imp[0].ext.ozone.lotameData.Profile.pid).to.equal('pid123'); + expect(payload.ext.ozone.lotameData.Profile.Audiences.Audience[0].id).to.equal('123abc'); + expect(payload.ext.ozone.lotameData.Profile.tpid).to.equal('123eeetpid'); + expect(payload.ext.ozone.lotameData.Profile.pid).to.equal('pid123'); expect(payload.ext.ozone.oz_lot_rw).to.equal(1); }); it('should pick up the value of valid lotame override parameters when there is NO "lotame" key at all', function () { @@ -1161,27 +2243,29 @@ describe('ozone Adapter', function () { spec.getGetParametersAsObject = function() { return {'oz_lotameid': '123abc', 'oz_lotamepid': 'pid123', 'oz_lotametpid': '123eeetpid'}; }; - const request = spec.buildRequests(nolotameBidReq, validBidderRequest); + const request = spec.buildRequests(nolotameBidReq, validBidderRequest.bidderRequest); const payload = JSON.parse(request.data); - expect(payload.imp[0].ext.ozone.lotameData.Profile.Audiences.Audience[0].id).to.equal('123abc'); - expect(payload.imp[0].ext.ozone.lotameData.Profile.tpid).to.equal('123eeetpid'); - expect(payload.imp[0].ext.ozone.lotameData.Profile.pid).to.equal('pid123'); + expect(payload.ext.ozone.lotameData.Profile.Audiences.Audience[0].id).to.equal('123abc'); + expect(payload.ext.ozone.lotameData.Profile.tpid).to.equal('123eeetpid'); + expect(payload.ext.ozone.lotameData.Profile.pid).to.equal('pid123'); expect(payload.ext.ozone.oz_lot_rw).to.equal(1); + spec.propertyBag = originalPropertyBag; // tidy up }); // NOTE - only one negative test case; // you can't send invalid lotame params to buildRequests because 'validate' will have rejected them it('should not use lotame override parameters if they dont exist', function () { + expect(spec.propertyBag.lotameWasOverridden).to.equal(0); spec.getGetParametersAsObject = function() { return {}; // no lotame override params }; - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const payload = JSON.parse(request.data); expect(payload.ext.ozone.oz_lot_rw).to.equal(0); }); it('should pick up the config value of coppa & set it in the request', function () { config.setConfig({'coppa': true}); - const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); + const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest.bidderRequest); const payload = JSON.parse(request.data); expect(payload.regs).to.include.keys('coppa'); expect(payload.regs.coppa).to.equal(1); @@ -1189,22 +2273,75 @@ describe('ozone Adapter', function () { }); it('should pick up the config value of coppa & only set it in the request if its true', function () { config.setConfig({'coppa': false}); - const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); + const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest.bidderRequest); const payload = JSON.parse(request.data); expect(utils.deepAccess(payload, 'regs.coppa')).to.be.undefined; config.resetConfig(); }); + it('should handle oz_omp_floor correctly', function () { + config.setConfig({'ozone': {'oz_omp_floor': 1.56}}); + const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest.bidderRequest); + const payload = JSON.parse(request.data); + expect(utils.deepAccess(payload, 'ext.ozone.oz_omp_floor')).to.equal(1.56); + config.resetConfig(); + }); + it('should ignore invalid oz_omp_floor values', function () { + config.setConfig({'ozone': {'oz_omp_floor': '1.56'}}); + const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest.bidderRequest); + const payload = JSON.parse(request.data); + expect(utils.deepAccess(payload, 'ext.ozone.oz_omp_floor')).to.be.undefined; + config.resetConfig(); + }); + it('should should contain a unique page view id in the auction request which persists across calls', function () { + let request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + let payload = JSON.parse(request.data); + expect(utils.deepAccess(payload, 'ext.ozone.pv')).to.be.a('string'); + request = spec.buildRequests(validBidRequests1OutstreamVideo2020, validBidderRequest.bidderRequest); + let payload2 = JSON.parse(request.data); + expect(utils.deepAccess(payload2, 'ext.ozone.pv')).to.be.a('string'); + expect(utils.deepAccess(payload2, 'ext.ozone.pv')).to.equal(utils.deepAccess(payload, 'ext.ozone.pv')); + }); + it('should indicate that the whitelist was used when it contains valid data', function () { + config.setConfig({'ozone': {'oz_whitelist_adserver_keys': ['oz_ozappnexus_pb', 'oz_ozappnexus_imp_id']}}); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.ext.ozone.oz_kvp_rw).to.equal(1); + config.resetConfig(); + }); + it('should indicate that the whitelist was not used when it contains no data', function () { + config.setConfig({'ozone': {'oz_whitelist_adserver_keys': []}}); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.ext.ozone.oz_kvp_rw).to.equal(0); + config.resetConfig(); + }); + it('should indicate that the whitelist was not used when it is not set in the config', function () { + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.ext.ozone.oz_kvp_rw).to.equal(0); + }); + it('should have openrtb video params', function() { + let allowed = ['mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', 'delivery', 'pos', 'companionad', 'api', 'companiontype', 'ext']; + const request = spec.buildRequests(validBidRequests1OutstreamVideo2020, validBidderRequest.bidderRequest); + const payload = JSON.parse(request.data); + const vid = (payload.imp[0].video); + const keys = Object.keys(vid); + for (let i = 0; i < keys.length; i++) { + expect(allowed).to.include(keys[i]); + } + expect(payload.imp[0].video.ext).to.include({'context': 'outstream'}); + }); }); describe('interpretResponse', function () { it('should build bid array', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const result = spec.interpretResponse(validResponse, request); expect(result.length).to.equal(1); }); it('should have all relevant fields', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const result = spec.interpretResponse(validResponse, request); const bid = result[0]; expect(bid.cpm).to.equal(validResponse.body.seatbid[0].bid[0].cpm); @@ -1213,16 +2350,15 @@ describe('ozone Adapter', function () { }); it('should build bid array with gdpr', function () { - let validBR = JSON.parse(JSON.stringify(bidderRequestWithFullGdpr)); + let validBR = JSON.parse(JSON.stringify(bidderRequestWithFullGdpr.bidderRequest)); validBR.gdprConsent = {'gdprApplies': 1, 'consentString': 'This is the gdpr consent string'}; const request = spec.buildRequests(validBidRequests, validBR); // works the old way, with GDPR not enforced by default - // const request = spec.buildRequests(validBidRequests, bidderRequestWithFullGdpr); // works with oz_enforceGdpr true by default const result = spec.interpretResponse(validResponse, request); expect(result.length).to.equal(1); }); it('should build bid array with only partial gdpr', function () { - var validBidderRequestWithGdpr = bidderRequestWithPartialGdpr; + var validBidderRequestWithGdpr = bidderRequestWithPartialGdpr.bidderRequest; validBidderRequestWithGdpr.gdprConsent = {'gdprApplies': 1, 'consentString': 'This is the gdpr consent string'}; const request = spec.buildRequests(validBidRequests, validBidderRequestWithGdpr); }); @@ -1239,24 +2375,164 @@ describe('ozone Adapter', function () { expect(result).to.be.empty; }); - it('should have video renderer', function () { - const request = spec.buildRequests(validBidRequestsWithNonBannerMediaTypesAndValidOutstreamVideo, validBidderRequest); - const result = spec.interpretResponse(validOutstreamResponse, request); + it('should have video renderer for outstream video', function () { + const request = spec.buildRequests(validBidRequests1OutstreamVideo2020, validBidderRequest1OutstreamVideo2020.bidderRequest); + const result = spec.interpretResponse(getCleanValidVideoResponse(), validBidderRequest1OutstreamVideo2020); const bid = result[0]; expect(bid.renderer).to.be.an.instanceOf(Renderer); }); + it('should have NO video renderer for instream video', function () { + let instreamRequestsObj = JSON.parse(JSON.stringify(validBidRequests1OutstreamVideo2020)); + instreamRequestsObj[0].mediaTypes.video.context = 'instream'; + let instreamBidderReq = JSON.parse(JSON.stringify(validBidderRequest1OutstreamVideo2020)); + instreamBidderReq.bidderRequest.bids[0].mediaTypes.video.context = 'instream'; + const request = spec.buildRequests(instreamRequestsObj, validBidderRequest1OutstreamVideo2020.bidderRequest); + const result = spec.interpretResponse(getCleanValidVideoResponse(), instreamBidderReq); + const bid = result[0]; + expect(bid.hasOwnProperty('renderer')).to.be.false; + }); + it('should correctly parse response where there are more bidders than ad slots', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const result = spec.interpretResponse(validBidResponse1adWith2Bidders, request); expect(result.length).to.equal(2); }); it('should have a ttl of 600', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const result = spec.interpretResponse(validResponse, request); expect(result[0].ttl).to.equal(300); }); + + it('should handle oz_omp_floor_dollars correctly, inserting 1 as necessary', function () { + config.setConfig({'ozone': {'oz_omp_floor': 0.01}}); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const result = spec.interpretResponse(validResponse, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_omp')).to.equal('1'); + config.resetConfig(); + }); + it('should handle oz_omp_floor_dollars correctly, inserting 0 as necessary', function () { + config.setConfig({'ozone': {'oz_omp_floor': 2.50}}); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const result = spec.interpretResponse(validResponse, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_omp')).to.equal('0'); + config.resetConfig(); + }); + it('should handle missing oz_omp_floor_dollars correctly, inserting nothing', function () { + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const result = spec.interpretResponse(validResponse, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_omp')).to.be.undefined; + }); + it('should handle ext.bidder.ozone.floor correctly, setting flr & rid as necessary', function () { + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + let vres = JSON.parse(JSON.stringify(validResponse)); + vres.body.seatbid[0].bid[0].ext.bidder.ozone = {floor: 1, ruleId: 'ZjbsYE1q'}; + const result = spec.interpretResponse(vres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_flr')).to.equal(1); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_rid')).to.equal('ZjbsYE1q'); + config.resetConfig(); + }); + it('should handle ext.bidder.ozone.floor correctly, inserting 0 as necessary', function () { + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + let vres = JSON.parse(JSON.stringify(validResponse)); + vres.body.seatbid[0].bid[0].ext.bidder.ozone = {floor: 0, ruleId: 'ZjbXXE1q'}; + const result = spec.interpretResponse(vres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_flr')).to.equal(0); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_rid')).to.equal('ZjbXXE1q'); + config.resetConfig(); + }); + it('should handle ext.bidder.ozone.floor correctly, inserting nothing as necessary', function () { + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + let vres = JSON.parse(JSON.stringify(validResponse)); + vres.body.seatbid[0].bid[0].ext.bidder.ozone = {}; + const result = spec.interpretResponse(vres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_flr', null)).to.equal(null); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_rid', null)).to.equal(null); + config.resetConfig(); + }); + it('should handle ext.bidder.ozone.floor correctly, when bidder.ozone is not there', function () { + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + let vres = JSON.parse(JSON.stringify(validResponse)); + const result = spec.interpretResponse(vres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_flr', null)).to.equal(null); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_rid', null)).to.equal(null); + config.resetConfig(); + }); + it('should handle a valid whitelist, removing items not on the list & leaving others', function () { + config.setConfig({'ozone': {'oz_whitelist_adserver_keys': ['oz_appnexus_crid', 'oz_appnexus_adId']}}); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const result = spec.interpretResponse(validResponse, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_adv')).to.be.undefined; + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_adId')).to.equal('2899ec066a91ff8-0-0'); + config.resetConfig(); + }); + it('should ignore a whitelist if enhancedAdserverTargeting is false', function () { + config.setConfig({'ozone': {'oz_whitelist_adserver_keys': ['oz_appnexus_crid', 'oz_appnexus_imp_id'], 'enhancedAdserverTargeting': false}}); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const result = spec.interpretResponse(validResponse, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_adv')).to.be.undefined; + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_imp_id')).to.be.undefined; + config.resetConfig(); + }); + it('should correctly handle enhancedAdserverTargeting being false', function () { + config.setConfig({'ozone': {'enhancedAdserverTargeting': false}}); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const result = spec.interpretResponse(validResponse, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_adv')).to.be.undefined; + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_imp_id')).to.be.undefined; + config.resetConfig(); + }); + it('should add flr into ads request if floor exists in the auction response', function () { + const request = spec.buildRequests(validBidRequestsMulti, validBidderRequest.bidderRequest); + let validres = JSON.parse(JSON.stringify(validResponse2Bids)); + validres.body.seatbid[0].bid[0].ext.bidder.ozone = {'floor': 1}; + const result = spec.interpretResponse(validres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_flr')).to.equal(1); + expect(utils.deepAccess(result[1].adserverTargeting, 'oz_appnexus_flr', '')).to.equal(''); + }); + it('should add rid into ads request if ruleId exists in the auction response', function () { + const request = spec.buildRequests(validBidRequestsMulti, validBidderRequest.bidderRequest); + let validres = JSON.parse(JSON.stringify(validResponse2Bids)); + validres.body.seatbid[0].bid[0].ext.bidder.ozone = {'ruleId': 123}; + const result = spec.interpretResponse(validres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_rid')).to.equal(123); + expect(utils.deepAccess(result[1].adserverTargeting, 'oz_appnexus_rid', '')).to.equal(''); + }); + it('should add oz_ozappnexus_sid (cid value) for all appnexus bids', function () { + const request = spec.buildRequests(validBidRequestsMulti, validBidderRequest.bidderRequest); + let validres = JSON.parse(JSON.stringify(validResponse2BidsSameAdunit)); + const result = spec.interpretResponse(validres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_ozappnexus_sid')).to.equal(result[0].cid); + }); + it('should add unique adId values to each bid', function() { + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + let validres = JSON.parse(JSON.stringify(validResponse2BidsSameAdunit)); + const result = spec.interpretResponse(validres, request); + expect(result.length).to.equal(1); + expect(result[0]['price']).to.equal(0.9); + expect(result[0]['adserverTargeting']['oz_ozappnexus_adId']).to.equal('2899ec066a91ff8-0-1'); + }); + it('should correctly process an auction with 2 adunits & multiple bidders one of which bids for both adslots', function() { + let validres = JSON.parse(JSON.stringify(multiResponse1)); + let request = spec.buildRequests(multiRequest1, multiBidderRequest1.bidderRequest); + let result = spec.interpretResponse(validres, request); + expect(result.length).to.equal(4); // one of the 5 bids will have been removed + expect(result[1]['price']).to.equal(0.521); + expect(result[1]['impid']).to.equal('3025f169863b7f8'); + expect(result[1]['id']).to.equal('18552976939844999'); + expect(result[1]['adserverTargeting']['oz_ozappnexus_adId']).to.equal('3025f169863b7f8-0-2'); + // change the bid values so a different second bid for an impid by the same bidder gets dropped + validres = JSON.parse(JSON.stringify(multiResponse1)); + validres.body.seatbid[0].bid[1].price = 1.1; + validres.body.seatbid[0].bid[1].cpm = 1.1; + request = spec.buildRequests(multiRequest1, multiBidderRequest1.bidderRequest); + result = spec.interpretResponse(validres, request); + expect(result[1]['price']).to.equal(1.1); + expect(result[1]['impid']).to.equal('3025f169863b7f8'); + expect(result[1]['id']).to.equal('18552976939844681'); + expect(result[1]['adserverTargeting']['oz_ozappnexus_adId']).to.equal('3025f169863b7f8-0-1'); + }); }); describe('userSyncs', function () { @@ -1270,7 +2546,7 @@ describe('ozone Adapter', function () { }); it('should append the various values if they exist', function() { // get the cookie bag populated - spec.buildRequests(validBidRequests, validBidderRequest); + spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const result = spec.getUserSyncs({iframeEnabled: true}, 'good server response', gdpr1); expect(result).to.be.an('array'); expect(result[0].url).to.include('publisherId=9876abcd12-3'); @@ -1383,83 +2659,7 @@ describe('ozone Adapter', function () { expect(result).to.be.false; config.resetConfig(); }); - it('should return true if oz_enforceGdpr is true and consentString is undefined', function() { - config.setConfig({'ozone': {'oz_enforceGdpr': true}}); - let req = JSON.parse(JSON.stringify(bidderRequestWithFullGdpr)); - delete req.gdprConsent.consentString; - let result = spec.blockTheRequest(req); - expect(result).to.be.true; - config.resetConfig(); - }); - it('should return false if oz_enforceGdpr is false and consentString is undefined', function() { - config.setConfig({'ozone': {'oz_enforceGdpr': false}}); - let req = JSON.parse(JSON.stringify(bidderRequestWithFullGdpr)); - delete req.gdprConsent.consentString; - let result = spec.blockTheRequest(req); - expect(result).to.be.false; - config.resetConfig(); - }); - it('should return false if oz_enforceGdpr is NOT SET (default) and consentString is undefined', function() { - let req = JSON.parse(JSON.stringify(bidderRequestWithFullGdpr)); - delete req.gdprConsent.consentString; - let result = spec.blockTheRequest(req); - expect(result).to.be.false; - }); - it('should return false if gdprApplies is false', function() { - config.setConfig({'ozone': {'oz_request': true}}); - let req = {'gdprConsent': {'gdprApplies': false}}; - let result = spec.blockTheRequest(req); - expect(result).to.be.false; - config.resetConfig(); - }); - it('should return false if gdprConsent key does not exist', function() { - let req = JSON.parse(JSON.stringify(bidderRequestWithFullGdpr)); - config.setConfig({'ozone': {'oz_enforceGdpr': true}}); - delete req.gdprConsent; - let result = spec.blockTheRequest(req); - expect(result).to.be.false; - config.resetConfig(); - }); - it('should return false if gdpr is set, and all is ok', function() { - let req = JSON.parse(JSON.stringify(bidderRequestWithFullGdpr)); - config.setConfig({'ozone': {'oz_enforceGdpr': true}}); - let result = spec.blockTheRequest(req); - expect(result).to.be.false; - config.resetConfig(); - }); - }); - - describe('failsGdprCheck', function() { - it('should return false for a a fully accepted user', function () { - let result = spec.failsGdprCheck(bidderRequestWithFullGdpr); - expect(result).to.be.false; - }); - it('should return false if gdprConsent is not present on the bidder object', function () { - let result = spec.failsGdprCheck(validBidderRequest); - expect(result).to.be.false; - }); - it('should return true if gdpr applies and vendorData is not an array', function () { - let req = JSON.parse(JSON.stringify(bidderRequestWithFullGdpr)); - req.gdprConsent.vendorData = null; - let result = spec.failsGdprCheck(req); - expect(result).to.be.true; - }); - it('should return true if gdpr applies and purposeConsents do not contain all the required true values', function () { - let req = JSON.parse(JSON.stringify(bidderRequestWithFullGdpr)); - req.gdprConsent.vendorData.purposeConsents[1] = false; - let result = spec.failsGdprCheck(req); - expect(result).to.be.true; - }); - it('should return true if gdpr applies and vendorConsents[524] is not true', function () { - config.setConfig({'ozone': {'oz_enforceGdpr': true}}); - let req = JSON.parse(JSON.stringify(bidderRequestWithFullGdpr)); - req.gdprConsent.vendorData.vendorConsents[524] = false; - let result = spec.failsGdprCheck(req); - expect(result).to.be.true; - config.resetConfig(); - }); }); - describe('makeLotameObjectFromOverride', function() { it('should update an object with valid lotame data', function () { let objLotameOverride = {'oz_lotametpid': '1234', 'oz_lotameid': '12345', 'oz_lotamepid': '123456'}; @@ -1498,4 +2698,125 @@ describe('ozone Adapter', function () { expect(result).to.be.false; }); }); + describe('getPageId', function() { + it('should return the same Page ID for multiple calls', function () { + let result = spec.getPageId(); + expect(result).to.be.a('string'); + let result2 = spec.getPageId(); + expect(result2).to.equal(result); + }); + }); + describe('getBidRequestForBidId', function() { + it('should locate a bid inside a bid array', function () { + let result = spec.getBidRequestForBidId('2899ec066a91ff8', validBidRequestsMulti); + expect(result.testId).to.equal(1); + result = spec.getBidRequestForBidId('2899ec066a91ff0', validBidRequestsMulti); + expect(result.testId).to.equal(2); + }); + }); + describe('getVideoContextForBidId', function() { + it('should locate the video context inside a bid', function () { + let result = spec.getVideoContextForBidId('2899ec066a91ff8', validBidRequestsWithNonBannerMediaTypesAndValidOutstreamVideo); + expect(result).to.equal('outstream'); + }); + }); + describe('getLotameOverrideParams', function() { + it('should get 3 valid lotame params that exist in GET params', function () { + // mock the getGetParametersAsObject function to simulate GET parameters for lotame overrides: + spec.getGetParametersAsObject = function() { + return {'oz_lotameid': '123abc', 'oz_lotamepid': 'pid123', 'oz_lotametpid': 'tpid123'}; + }; + let result = spec.getLotameOverrideParams(); + expect(Object.keys(result).length).to.equal(3); + }); + it('should get only 1 valid lotame param that exists in GET params', function () { + // mock the getGetParametersAsObject function to simulate GET parameters for lotame overrides: + spec.getGetParametersAsObject = function() { + return {'oz_lotameid': '123abc', 'xoz_lotamepid': 'pid123', 'xoz_lotametpid': 'tpid123'}; + }; + let result = spec.getLotameOverrideParams(); + expect(Object.keys(result).length).to.equal(1); + }); + }); + describe('unpackVideoConfigIntoIABformat', function() { + it('should correctly unpack a usual video config', function () { + let mediaTypes = { + playerSize: [640, 480], + mimes: ['video/mp4'], + context: 'outstream', + testKey: 'parent value' + }; + let bid_params_video = { + skippable: true, + playback_method: ['auto_play_sound_off'], + playbackmethod: 2, /* start on load, no sound */ + minduration: 5, + maxduration: 60, + skipmin: 5, + skipafter: 5, + testKey: 'child value' + }; + let result = spec.unpackVideoConfigIntoIABformat(mediaTypes, bid_params_video); + expect(result.mimes).to.be.an('array').that.includes('video/mp4'); + expect(result.ext.context).to.equal('outstream'); + expect(result.ext.skippable).to.be.true; // note - we add skip in a different step: addVideoDefaults + expect(result.ext.testKey).to.equal('child value'); + }); + }); + describe('addVideoDefaults', function() { + it('should correctly add video defaults', function () { + let mediaTypes = { + playerSize: [640, 480], + mimes: ['video/mp4'], + context: 'outstream', + }; + let bid_params_video = { + skippable: true, + playback_method: ['auto_play_sound_off'], + playbackmethod: 2, /* start on load, no sound */ + minduration: 5, + maxduration: 60, + skipmin: 5, + skipafter: 5, + testKey: 'child value' + }; + let result = spec.addVideoDefaults({}, mediaTypes, mediaTypes); + expect(result.placement).to.equal(3); + expect(result.skip).to.equal(0); + result = spec.addVideoDefaults({}, mediaTypes, bid_params_video); + expect(result.skip).to.equal(1); + }); + it('should correctly add video defaults including skippable in parent', function () { + let mediaTypes = { + playerSize: [640, 480], + mimes: ['video/mp4'], + context: 'outstream', + skippable: true + }; + let bid_params_video = { + playback_method: ['auto_play_sound_off'], + playbackmethod: 2, /* start on load, no sound */ + minduration: 5, + maxduration: 60, + skipmin: 5, + skipafter: 5, + testKey: 'child value' + }; + let result = spec.addVideoDefaults({}, mediaTypes, bid_params_video); + expect(result.placement).to.equal(3); + expect(result.skip).to.equal(1); + }); + }); + describe('removeSingleBidderMultipleBids', function() { + it('should remove the multi bid by ozappnexus for adslot 2d30e86db743a8', function() { + let validres = JSON.parse(JSON.stringify(multiResponse1)); + expect(validres.body.seatbid[0].bid.length).to.equal(3); + expect(validres.body.seatbid[0].seat).to.equal('ozappnexus'); + let response = spec.removeSingleBidderMultipleBids(validres.body.seatbid); + expect(response.length).to.equal(2); + expect(response[0].bid.length).to.equal(2); + expect(response[0].seat).to.equal('ozappnexus'); + expect(response[1].bid.length).to.equal(2); + }); + }); }); From 1ea0d26b761be239fa9a73f4b7dfd29660f62865 Mon Sep 17 00:00:00 2001 From: relaido <63339139+relaido@users.noreply.github.com> Date: Fri, 10 Jul 2020 00:30:52 +0900 Subject: [PATCH 36/39] Change endpoint (#5459) * add relaido adapter * remove event listener * fixed UserSyncs and e.data * fix conflicts * change endpoint url Co-authored-by: ishigami_shingo --- modules/relaidoBidAdapter.js | 2 +- test/spec/modules/relaidoBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js index a2c97495a7e..b3b8a647137 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -45,7 +45,7 @@ function buildRequests(validBidRequests, bidderRequest) { const bidRequest = validBidRequests[i]; const placementId = utils.getBidIdParameter('placementId', bidRequest.params); const bidDomain = bidRequest.params.domain || BIDDER_DOMAIN; - const bidUrl = `https://${bidDomain}/vast/v1/out/bid/${placementId}`; + const bidUrl = `https://${bidDomain}/bid/v1/prebid/${placementId}`; const uuid = getUuid(); const mediaType = getMediaType(bidRequest); diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 492f7d2ca08..cd4918460db 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -136,7 +136,7 @@ describe('RelaidoAdapter', function () { expect(bidRequests).to.have.lengthOf(1); const request = bidRequests[0]; expect(request.method).to.equal('GET'); - expect(request.url).to.equal('https://api.relaido.jp/vast/v1/out/bid/100000'); + expect(request.url).to.equal('https://api.relaido.jp/bid/v1/prebid/100000'); expect(request.bidId).to.equal(bidRequest.bidId); expect(request.width).to.equal(bidRequest.mediaTypes.video.playerSize[0][0]); expect(request.height).to.equal(bidRequest.mediaTypes.video.playerSize[0][1]); From 8f17bb51e444197133d3b645827adfb280c9b43f Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 9 Jul 2020 15:14:33 -0400 Subject: [PATCH 37/39] Add lotame id system (#5388) * Add a LotameIdSystem as a new User ID module * Now using our own caching system * Add more test cases; Switch names to be constants * Add test cases * Handle "expiring" local storage like the built in code does * Switch to using the expiry_ms from the endpoint as the cookie expiration * Update to the official naming; Remove the lastUpdate storage as we don't use it and depend on what comes from the endpoint; Update to use the dev bcp endpoint for now * Fix tests, numbers vs strings....; Add gdpr_applies query param * Fix the timestamp becoming an invalid date * Clear out the panorama id when on optout * Add eid support * Switch to the prod version of the url * Update test wording * From PR feedback Only care about the core_id if a profile_id is also returned so it won't look like we can store a core_id and then promptly delete it Return just the core_id from getId() * Add eid test for lotamePanoramaId --- integrationExamples/gpt/userId_example.html | 9 +- modules/.submodules.json | 1 + modules/lotamePanoramaIdSystem.js | 244 ++++++++++ modules/lotamePanoramaIdSystem.md | 25 + modules/userId/eids.js | 6 + test/spec/modules/eids_spec.js | 12 + .../modules/lotamePanoramaIdSystem_spec.js | 449 ++++++++++++++++++ 7 files changed, 744 insertions(+), 2 deletions(-) create mode 100644 modules/lotamePanoramaIdSystem.js create mode 100644 modules/lotamePanoramaIdSystem.md create mode 100644 test/spec/modules/lotamePanoramaIdSystem_spec.js diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index 53c58e8e87e..69e2c713fb8 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -179,7 +179,8 @@ name: 'idl_env', expires: 30 } - }, { + }, + { name: "sharedId", params: { syncTime: 60 // in seconds, default is 24 hours @@ -189,7 +190,11 @@ name: "sharedid", expires: 28 } - }, { + }, + { + name: 'lotamePanoramaId' + }, + { name: "liveIntentId", params: { publisherId: "9896876" diff --git a/modules/.submodules.json b/modules/.submodules.json index 25ae3c3884b..ba8af4e6550 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -7,6 +7,7 @@ "parrableIdSystem", "britepoolIdSystem", "liveIntentIdSystem", + "lotamePanoramaId", "criteoIdSystem", "netIdSystem", "identityLinkIdSystem", diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js new file mode 100644 index 00000000000..cdf9131dd68 --- /dev/null +++ b/modules/lotamePanoramaIdSystem.js @@ -0,0 +1,244 @@ +/** + * This module adds LotamePanoramaId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/lotamePanoramaId + * @requires module:modules/userId + */ +import * as utils from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const KEY_ID = 'panoramaId'; +const KEY_EXPIRY = `${KEY_ID}_expiry`; +const KEY_PROFILE = '_cc_id'; +const MODULE_NAME = 'lotamePanoramaId'; +const NINE_MONTHS_MS = 23328000 * 1000; +const DAYS_TO_CACHE = 7; +const DAY_MS = 60 * 60 * 24 * 1000; + +export const storage = getStorageManager(null, MODULE_NAME); + +/** + * Set the Lotame First Party Profile ID in the first party namespace + * @param {String} profileId + */ +function setProfileId(profileId) { + if (storage.cookiesAreEnabled()) { + let expirationDate = new Date(utils.timestamp() + NINE_MONTHS_MS).toUTCString(); + storage.setCookie(KEY_PROFILE, profileId, expirationDate, 'Lax', undefined, undefined); + } + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage(KEY_PROFILE, profileId, undefined); + } +} + +/** + * Get the Lotame profile id by checking cookies first and then local storage + */ +function getProfileId() { + if (storage.cookiesAreEnabled()) { + return storage.getCookie(KEY_PROFILE, undefined); + } + if (storage.hasLocalStorage()) { + return storage.getDataFromLocalStorage(KEY_PROFILE, undefined); + } +} + +/** + * Get a value from browser storage by checking cookies first and then local storage + * @param {String} key + */ +function getFromStorage(key) { + let value = null; + if (storage.cookiesAreEnabled()) { + value = storage.getCookie(key, undefined); + } + if (storage.hasLocalStorage() && value === null) { + const storedValueExp = storage.getDataFromLocalStorage( + `${key}_exp`, undefined + ); + if (storedValueExp === '') { + value = storage.getDataFromLocalStorage(key, undefined); + } else if (storedValueExp) { + if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { + value = storage.getDataFromLocalStorage(key, undefined); + } + } + } + return value; +} + +/** + * Save a key/value pair to the browser cache (cookies and local storage) + * @param {String} key + * @param {String} value + * @param {Number} expirationTimestamp + */ +function saveLotameCache( + key, + value, + expirationTimestamp = utils.timestamp() + DAYS_TO_CACHE * DAY_MS +) { + if (key && value) { + let expirationDate = new Date(expirationTimestamp).toUTCString(); + if (storage.cookiesAreEnabled()) { + storage.setCookie( + key, + value, + expirationDate, + 'Lax', + undefined, + undefined + ); + } + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage( + `${key}_exp`, + String(expirationTimestamp), + undefined + ); + storage.setDataInLocalStorage(key, value, undefined); + } + } +} + +/** + * Retrieve all the cached values from cookies and/or local storage + */ +function getLotameLocalCache() { + let cache = { + data: getFromStorage(KEY_ID), + expiryTimestampMs: 0, + }; + + try { + const rawExpiry = getFromStorage(KEY_EXPIRY); + if (utils.isStr(rawExpiry)) { + cache.expiryTimestampMs = parseInt(rawExpiry, 0); + } + } catch (error) { + utils.logError(error); + } + + return cache; +} + +/** + * Clear a cached value from cookies and local storage + * @param {String} key + */ +function clearLotameCache(key) { + if (key) { + if (storage.cookiesAreEnabled()) { + let expirationDate = new Date(0).toUTCString(); + storage.setCookie(key, '', expirationDate, 'Lax', undefined, undefined); + } + if (storage.hasLocalStorage()) { + storage.removeDataFromLocalStorage(key, undefined); + } + } +} +/** @type {Submodule} */ +export const lotamePanoramaIdSubmodule = { + + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * Decode the stored id value for passing to bid requests + * @function decode + * @param {(Object|string)} value + * @returns {(Object|undefined)} + */ + decode(value, configParams) { + return utils.isStr(value) ? { 'lotamePanoramaId': value } : undefined; + }, + + /** + * Retrieve the Lotame Panorama Id + * @function + * @param {SubmoduleParams} [configParams] + * @param {ConsentData} [consentData] + * @param {(Object|undefined)} cacheIdObj + * @returns {IdResponse|undefined} + */ + getId(configParams, consentData, cacheIdObj) { + let localCache = getLotameLocalCache(); + + let refreshNeeded = Date.now() > localCache.expiryTimestampMs; + + if (!refreshNeeded) { + return { + id: localCache.data + }; + } + + const storedUserId = getProfileId(); + + const resolveIdFunction = function (callback) { + let queryParams = {}; + if (storedUserId) { + queryParams.fp = storedUserId + } + + if (consentData && utils.isBoolean(consentData.gdprApplies)) { + queryParams.gdpr_applies = consentData.gdprApplies; + if (consentData.gdprApplies) { + queryParams.gdpr_consent = consentData.consentString; + } + } + const url = utils.buildUrl({ + protocol: 'https', + host: `id.crwdcntrl.net`, + pathname: '/id', + search: utils.isEmpty(queryParams) ? undefined : queryParams, + }); + ajax( + url, + (response) => { + let coreId; + if (response) { + try { + let responseObj = JSON.parse(response); + saveLotameCache(KEY_EXPIRY, responseObj.expiry_ts); + + if (utils.isStr(responseObj.profile_id)) { + setProfileId(responseObj.profile_id); + + if (utils.isStr(responseObj.core_id)) { + saveLotameCache( + KEY_ID, + responseObj.core_id, + responseObj.expiry_ts + ); + coreId = responseObj.core_id; + } else { + clearLotameCache(KEY_ID); + } + } else { + clearLotameCache(KEY_PROFILE); + clearLotameCache(KEY_ID); + } + } catch (error) { + utils.logError(error); + } + } + callback(coreId); + }, + undefined, + { + method: 'GET', + withCredentials: true + } + ); + }; + + return { callback: resolveIdFunction }; + }, +}; + +submodule('userId', lotamePanoramaIdSubmodule); diff --git a/modules/lotamePanoramaIdSystem.md b/modules/lotamePanoramaIdSystem.md new file mode 100644 index 00000000000..e960f4b5695 --- /dev/null +++ b/modules/lotamePanoramaIdSystem.md @@ -0,0 +1,25 @@ +# Overview + +``` +Module Name: Lotame Panorama Id System +Module Type: Id System +Maintainer: prebid@lotame.com +``` + +# Description + +Retrieve the Lotame Panorama Id + +# Usage + +``` + pbjs.que.push(function() { + pbjs.setConfig({ + usersync: { + userIds: [ + { + name: 'lotamePanoramaId' // The only parameter that is needed + }], + } + }); +``` \ No newline at end of file diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 842737183a8..1261e2ea7d9 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -62,6 +62,12 @@ const USER_IDS_CONFIG = { atype: 1 }, + // lotamePanoramaId + lotamePanoramaId: { + source: 'crwdcntrl.net', + atype: 1, + }, + // DigiTrust 'digitrustid': { getValue: function (data) { diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 160277204df..1cbc2911ef5 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -107,6 +107,18 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('lotamePanoramaId', function () { + const userId = { + lotamePanoramaId: 'some-random-id-value', + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'crwdcntrl.net', + uids: [{ id: 'some-random-id-value', atype: 1 }], + }); + }); + it('DigiTrust; getValue call', function() { const userId = { digitrustid: { diff --git a/test/spec/modules/lotamePanoramaIdSystem_spec.js b/test/spec/modules/lotamePanoramaIdSystem_spec.js new file mode 100644 index 00000000000..c6d3383374a --- /dev/null +++ b/test/spec/modules/lotamePanoramaIdSystem_spec.js @@ -0,0 +1,449 @@ +import { + lotamePanoramaIdSubmodule, + storage, +} from 'modules/lotamePanoramaIdSystem.js'; +import * as utils from 'src/utils.js'; +import { server } from 'test/mocks/xhr.js'; + +const responseHeader = { 'Content-Type': 'application/json' }; + +describe('LotameId', function() { + let logErrorStub; + let getCookieStub; + let setCookieStub; + let getLocalStorageStub; + let setLocalStorageStub; + let removeFromLocalStorageStub; + let timeStampStub; + + const nowTimestamp = new Date().getTime(); + + beforeEach(function () { + logErrorStub = sinon.stub(utils, 'logError'); + getCookieStub = sinon.stub(storage, 'getCookie'); + setCookieStub = sinon.stub(storage, 'setCookie'); + getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + setLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); + removeFromLocalStorageStub = sinon.stub( + storage, + 'removeDataFromLocalStorage' + ); + timeStampStub = sinon.stub(utils, 'timestamp').returns(nowTimestamp); + }); + + afterEach(function () { + logErrorStub.restore(); + getCookieStub.restore(); + setCookieStub.restore(); + getLocalStorageStub.restore(); + setLocalStorageStub.restore(); + removeFromLocalStorageStub.restore(); + timeStampStub.restore(); + }); + + describe('caching initial data received from the remote server', function () { + let request; + let callBackSpy = sinon.spy(); + + beforeEach(function() { + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}).callback; + submoduleCallback(callBackSpy); + + request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ + profile_id: '4ec137245858469eb94a4e248f238694', + expiry_ts: 10, + core_id: + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87a', + }) + ); + }); + + it('should call the remote server when getId is called', function () { + expect(request.url).to.be.eq('https://id.crwdcntrl.net/id'); + + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should save the first party id', function () { + sinon.assert.calledWith( + setLocalStorageStub, + '_cc_id', + '4ec137245858469eb94a4e248f238694' + ); + sinon.assert.calledWith( + setCookieStub, + '_cc_id', + '4ec137245858469eb94a4e248f238694' + ); + }); + + it('should save the expiry', function () { + sinon.assert.calledWith( + setLocalStorageStub, + 'panoramaId_expiry', 10); + + sinon.assert.calledWith( + setCookieStub, + 'panoramaId_expiry', 10 + ); + }); + + it('should save the id', function () { + sinon.assert.calledWith( + setLocalStorageStub, + 'panoramaId', + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87a' + ); + + sinon.assert.calledWith( + setCookieStub, + 'panoramaId', + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87a' + ); + }); + }); + + describe('No stored values', function() { + describe('and receives the profile id but no panorama id', function() { + let request; + let callBackSpy = sinon.spy(); + + beforeEach(function() { + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}).callback; + submoduleCallback(callBackSpy); + request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ + profile_id: '4ec137245858469eb94a4e248f238694', + expiry_ts: 3800000, + }) + ); + }); + + it('should save the profile id', function() { + sinon.assert.calledWith( + setLocalStorageStub, + '_cc_id', + '4ec137245858469eb94a4e248f238694' + ); + sinon.assert.calledWith( + setCookieStub, + '_cc_id', + '4ec137245858469eb94a4e248f238694' + ); + }); + + it('should save the panorama id expiry', function () { + sinon.assert.calledWith( + setLocalStorageStub, + 'panoramaId_expiry', + 3800000 + ); + + sinon.assert.calledWith(setCookieStub, 'panoramaId_expiry', 3800000); + }); + + it('should NOT save the panorama id', function () { + sinon.assert.neverCalledWith( + setLocalStorageStub, + 'panoramaId', + sinon.match.any + ); + + sinon.assert.calledWith( + removeFromLocalStorageStub, + 'panoramaId' + ); + + sinon.assert.calledWith( + setCookieStub, + 'panoramaId', + '', + 'Thu, 01 Jan 1970 00:00:00 GMT', + 'Lax' + ); + }); + }); + + describe('and receives both the profile id and the panorama id', function () { + let request; + let callBackSpy = sinon.spy(); + + beforeEach(function () { + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}).callback; + submoduleCallback(callBackSpy); + request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ + profile_id: '4ec137245858469eb94a4e248f238694', + core_id: + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87b', + expiry_ts: 3600000, + }) + ); + }); + it('should save the profile id', function () { + sinon.assert.calledWith( + setLocalStorageStub, + '_cc_id', + '4ec137245858469eb94a4e248f238694' + ); + sinon.assert.calledWith( + setCookieStub, + '_cc_id', + '4ec137245858469eb94a4e248f238694' + ); + }); + + it('should save the panorama id expiry', function () { + sinon.assert.calledWith( + setLocalStorageStub, + 'panoramaId_expiry', + 3600000 + ); + + sinon.assert.calledWith(setCookieStub, 'panoramaId_expiry', 3600000); + }); + + it('should save the panorama id', function () { + sinon.assert.calledWith( + setLocalStorageStub, + 'panoramaId', + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87b' + ); + + sinon.assert.calledWith( + setCookieStub, + 'panoramaId', + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87b' + ); + }); + }); + }); + + describe('With a panorama id found', function() { + describe('and it is too early to try again', function () { + let submoduleCallback; + + beforeEach(function () { + getCookieStub + .withArgs('panoramaId_expiry') + .returns(String(Date.now() + 100000)); + getCookieStub + .withArgs('panoramaId') + .returns( + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87c' + ); + + submoduleCallback = lotamePanoramaIdSubmodule.getId({}); + }); + + it('should not call the remote server when getId is called', function () { + expect(submoduleCallback).to.be.eql({ + id: 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87c', + }); + }); + }); + + describe('and can try again', function () { + let request; + let callBackSpy = sinon.spy(); + + beforeEach(function () { + getCookieStub.withArgs('panoramaId_expiry').returns('1000'); + getCookieStub + .withArgs('panoramaId') + .returns( + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87d' + ); + + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}).callback; + submoduleCallback(callBackSpy); + + request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ + profile_id: '4ec137245858469eb94a4e248f238694', + core_id: + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87d', + expiry_ts: 3600000 + }) + ); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + }); + + describe('receives an optout request', function () { + let request; + let callBackSpy = sinon.spy(); + + beforeEach(function () { + getCookieStub.withArgs('panoramaId_expiry').returns('1000'); + getCookieStub + .withArgs('panoramaId') + .returns( + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87d' + ); + + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}).callback; + submoduleCallback(callBackSpy); + + request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ + expiry_ts: Date.now() + (30 * 24 * 60 * 60 * 1000), + }) + ); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should clear the panorama id', function () { + sinon.assert.calledWith( + removeFromLocalStorageStub, + 'panoramaId' + ); + + sinon.assert.calledWith( + setCookieStub, + 'panoramaId', + '', + 'Thu, 01 Jan 1970 00:00:00 GMT', + 'Lax' + ); + }); + + it('should clear the profile id', function () { + sinon.assert.calledWith(removeFromLocalStorageStub, '_cc_id'); + + sinon.assert.calledWith( + setCookieStub, + '_cc_id', + '', + 'Thu, 01 Jan 1970 00:00:00 GMT', + 'Lax' + ); + }); + }); + }); + + describe('With no panorama id found', function() { + beforeEach(function() { + getCookieStub.withArgs('panoramaId').returns(null); + getLocalStorageStub.withArgs('panoramaId').returns(null); + }) + describe('and it is too early to try again', function () { + let submoduleCallback; + + beforeEach(function () { + getCookieStub + .withArgs('panoramaId_expiry') + .returns(String(Date.now() + 100000)); + + submoduleCallback = lotamePanoramaIdSubmodule.getId({}); + }); + + it('should not call the remote server when getId is called', function () { + expect(submoduleCallback).to.be.eql({ + id: null + }); + }); + }); + + describe('and can try again', function () { + let request; + let callBackSpy = sinon.spy(); + + beforeEach(function () { + getLocalStorageStub + .withArgs('panoramaId_expiry') + .returns('1000'); + + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}).callback; + submoduleCallback(callBackSpy); + + request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ + profile_id: '4ec137245858469eb94a4e248f238694', + core_id: + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87e', + expiry_ts: 3600000 + }) + ); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + }); + }); + + describe('when gdpr applies', function () { + let request; + let callBackSpy = sinon.spy(); + + beforeEach(function () { + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, { + gdprApplies: true, + consentString: 'consentGiven' + }).callback; + submoduleCallback(callBackSpy); + + request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ + profile_id: '4ec137245858469eb94a4e248f238694', + core_id: + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87f', + expiry_ts: 3600000, + }) + ); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should pass the gdpr consent string back', function() { + expect(request.url).to.be.eq( + 'https://id.crwdcntrl.net/id?gdpr_applies=true&gdpr_consent=consentGiven' + ); + }); + }); + + it('should retrieve the id when decode is called', function() { + var id = lotamePanoramaIdSubmodule.decode('1234'); + expect(id).to.be.eql({ + 'lotamePanoramaId': '1234' + }); + }); +}); From 0f706bad95190e908aca4b9ea952e8105d90e31a Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Thu, 9 Jul 2020 16:04:57 -0400 Subject: [PATCH 38/39] Prebid 3.25.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db35edf3b9d..bcc25d215ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.25.0-pre", + "version": "3.25.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 656b352d39c7324797c5c191fa176850aef7c1f6 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Thu, 9 Jul 2020 16:18:28 -0400 Subject: [PATCH 39/39] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bcc25d215ec..4eb14fc5144 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.25.0", + "version": "3.26.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": {