From 4f09a32a8f8062ac3f836ef8ec08ac496b816134 Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 20 Apr 2023 15:29:52 +0200 Subject: [PATCH 01/10] New adapter: concepx --- modules/conceptxBidAdapter.js | 72 ++++++++++ modules/conceptxBidAdapter.md | 36 +++++ test/spec/modules/conceptxBidAdapter_spec.js | 136 +++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 modules/conceptxBidAdapter.js create mode 100644 modules/conceptxBidAdapter.md create mode 100644 test/spec/modules/conceptxBidAdapter_spec.js diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js new file mode 100644 index 00000000000..56aed4a843e --- /dev/null +++ b/modules/conceptxBidAdapter.js @@ -0,0 +1,72 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +// import { logError, logInfo, logWarn, parseUrl } from '../src/utils.js'; + +const BIDDER_CODE = 'conceptx'; +let ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; +// const LOG_PREFIX = 'ConceptX: '; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid: function (bid) { + return !!(bid.bidId); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + // logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); + const requests = []; + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + ENDPOINT_URL += '?gdpr_applies=' + bidderRequest.gdprConsent.gdprApplies; + ENDPOINT_URL += '&consentString=' + bidderRequest.gdprConsent.consentString; + } + for (var i = 0; i < validBidRequests.length; i++) { + const requestParent = { adUnits: [], meta: {} }; + const bid = validBidRequests[i] + const { adUnitCode, auctionId, bidId, bidder, bidderRequestId, ortb2 } = bid + requestParent.meta = { + adUnitCode, auctionId, bidId, bidder, bidderRequestId, ortb2 + } + + const { site, adunit } = bid.params + const adUnit = { site, adunit, targetId: bid.bidId } + if (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) adUnit.dimensions = bid.mediaTypes.banner.sizes + requestParent.adUnits.push(adUnit); + requests.push({ + method: 'POST', + url: ENDPOINT_URL, + options: { + withCredentials: false, + }, + data: JSON.stringify(requestParent), + }); + } + + return requests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + const bidResponsesFromServer = serverResponse.body.bidResponses; + const firstDummy = bidResponsesFromServer[0] + const firstSeat = firstDummy.ads[0] + const bidResponses = []; + const bidResponse = { + requestId: firstSeat.requestId, + cpm: firstSeat.cpm, + width: firstSeat.width, + height: firstSeat.height, + creativeId: firstSeat.creativeId, + dealId: firstSeat.dealId, + currency: firstSeat.currency, + netRevenue: true, + ttl: firstSeat.ttl, + referrer: firstSeat.referrer, + ad: firstSeat.html + }; + bidResponses.push(bidResponse); + return bidResponses; + }, + +} +registerBidder(spec); diff --git a/modules/conceptxBidAdapter.md b/modules/conceptxBidAdapter.md new file mode 100644 index 00000000000..1464c04025a --- /dev/null +++ b/modules/conceptxBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: ConceptX Bidder Adapter +Module Type: Bidder Adapter +Maintainer: info@concept.dk +``` + +# Description + +ConceptX Bidder Adapter for Prebid.js. +Only Banner format is supported. + +# Test Parameters +``` + var adUnits = [ + { + code: "test-div", + mediaTypes: { + banner: { + sizes: [[980, 180]] + } + }, + bids: [ + { + bidder: "conceptx", + params: { + site: "example", + adunit: "some-id-3", + } + }, + ] + }, + + ]; +``` diff --git a/test/spec/modules/conceptxBidAdapter_spec.js b/test/spec/modules/conceptxBidAdapter_spec.js new file mode 100644 index 00000000000..349ee765b71 --- /dev/null +++ b/test/spec/modules/conceptxBidAdapter_spec.js @@ -0,0 +1,136 @@ +// import or require modules necessary for the test, e.g.: +import { expect } from 'chai'; // may prefer 'assert' in place of 'expect' +import { spec } from 'modules/conceptxBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +// import { config } from 'src/config.js'; + +describe('conceptxBidAdapter', function () { + const URL = 'https://conceptx.cncpt-central.com/openrtb'; + + // before(() => { + + // }); + + // after(() => { + // // $$PREBID_GLOBAL$$.bidderSettings = {}; + // }); + + // afterEach(function () { + // config.resetConfig(); + // }); + + const ENDPOINT_URL = `${URL}`; + const ENDPOINT_URL_CONSENT = `${URL}?gdpr_applies=true&consentString=ihaveconsented`; + const adapter = newBidder(spec); + + const bidderRequests = [ + { + bidId: '123', + bidder: 'conceptx', + params: { + site: 'example', + adunit: 'some-id-3' + }, + mediaTypes: { + banner: { + sizes: [[930, 180]], + } + }, + } + ] + + const singleBidRequest = { + bid: [ + { + bidId: '123', + } + ] + } + + const serverResponse = { + body: { + 'bidResponses': [ + { + 'ads': [ + { + 'referrer': 'http://localhost/prebidpage_concept_bidder.html', + 'ttl': 360, + 'html': '

DUMMY

', + 'requestId': '214dfadd1f8826', + 'cpm': 46, + 'currency': 'DKK', + 'width': 930, + 'height': 180, + 'creativeId': 'FAKE-ID', + 'meta': { + 'mediaType': 'banner' + }, + 'netRevenue': true, + 'destinationUrls': { + 'destination': 'https://concept.dk' + } + } + ], + 'matchedAdCount': 1, + 'targetId': '214dfadd1f8826' + } + ] + } + } + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bidderRequests[0])).to.equal(true); + }); + }); + + describe('buildRequests', function () { + it('Test requests', function () { + const request = spec.buildRequests(bidderRequests, {}); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('data'); + const bid = JSON.parse(request[0].data).adUnits[0] + expect(bid.site).to.equal('example'); + expect(bid.adunit).to.equal('some-id-3'); + expect(JSON.stringify(bid.dimensions)).to.equal(JSON.stringify([ + [930, 180]])); + }); + }); + + describe('user privacy', function () { + it('should NOT send GDPR Consent data if gdprApplies equals undefined', function () { + let request = spec.buildRequests(bidderRequests, { gdprConsent: { gdprApplies: undefined, consentString: 'iDoNotConsent' } }); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url') + expect(request[0].url).to.equal(ENDPOINT_URL); + }); + it('should send GDPR Consent data if gdprApplies', function () { + let request = spec.buildRequests(bidderRequests, { gdprConsent: { gdprApplies: true, consentString: 'ihaveconsented' } }); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url') + expect(request[0].url).to.equal(ENDPOINT_URL_CONSENT); + }); + }); + + describe('interpretResponse', function () { + it('should return valid response when passed valid server response', function () { + const interpretedResponse = spec.interpretResponse(serverResponse, singleBidRequest); + const ad = serverResponse.body.bidResponses[0].ads[0] + expect(interpretedResponse).to.have.lengthOf(1); + expect(interpretedResponse[0].cpm).to.equal(ad.cpm); + expect(interpretedResponse[0].width).to.equal(Number(ad.width)); + expect(interpretedResponse[0].height).to.equal(Number(ad.height)); + expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); + expect(interpretedResponse[0].currency).to.equal(ad.currency); + expect(interpretedResponse[0].netRevenue).to.equal(true); + expect(interpretedResponse[0].ad).to.equal(ad.html); + expect(interpretedResponse[0].ttl).to.equal(360); + }); + }); +}); From 6a0d44d3d312f8d3c5ace073759a49bdb2f0f5fd Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 24 May 2023 10:28:29 +0200 Subject: [PATCH 02/10] Syntax change --- modules/conceptxBidAdapter.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index 56aed4a843e..e331c411bc2 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -24,13 +24,12 @@ export const spec = { for (var i = 0; i < validBidRequests.length; i++) { const requestParent = { adUnits: [], meta: {} }; const bid = validBidRequests[i] - const { adUnitCode, auctionId, bidId, bidder, bidderRequestId, ortb2 } = bid - requestParent.meta = { - adUnitCode, auctionId, bidId, bidder, bidderRequestId, ortb2 - } + + requestParent.meta = { adUnitCode, auctionId, bidId, bidder, bidderRequestId, ortb2 } = bid; const { site, adunit } = bid.params const adUnit = { site, adunit, targetId: bid.bidId } + if (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) adUnit.dimensions = bid.mediaTypes.banner.sizes requestParent.adUnits.push(adUnit); requests.push({ From 958fce6c07ce0770abc1afee903e645efa1f047b Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 24 May 2023 11:02:55 +0200 Subject: [PATCH 03/10] Revert syntax change --- modules/conceptxBidAdapter.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index e331c411bc2..1147a50e71f 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -24,12 +24,11 @@ export const spec = { for (var i = 0; i < validBidRequests.length; i++) { const requestParent = { adUnits: [], meta: {} }; const bid = validBidRequests[i] - - requestParent.meta = { adUnitCode, auctionId, bidId, bidder, bidderRequestId, ortb2 } = bid; + const { adUnitCode, auctionId, bidId, bidder, bidderRequestId, ortb2 } = bid + requestParent.meta = { adUnitCode, auctionId, bidId, bidder, bidderRequestId, ortb2 } const { site, adunit } = bid.params const adUnit = { site, adunit, targetId: bid.bidId } - if (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) adUnit.dimensions = bid.mediaTypes.banner.sizes requestParent.adUnits.push(adUnit); requests.push({ From 6e58c502215ce156ccd620c34802a90e02bdeeb1 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 21 Jun 2023 13:59:51 +0200 Subject: [PATCH 04/10] Defensive check for response from bidder server --- modules/conceptxBidAdapter.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index 1147a50e71f..127b049bc99 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -45,10 +45,13 @@ export const spec = { }, interpretResponse: function (serverResponse, bidRequest) { - const bidResponsesFromServer = serverResponse.body.bidResponses; - const firstDummy = bidResponsesFromServer[0] - const firstSeat = firstDummy.ads[0] const bidResponses = []; + const bidResponsesFromServer = serverResponse.body.bidResponses; + if (Array.isArray(bidResponsesFromServer) && bidResponsesFromServer.length === 0) { + return bidResponses + } + const firstBid = bidResponsesFromServer[0] + const firstSeat = firstBid.ads[0] const bidResponse = { requestId: firstSeat.requestId, cpm: firstSeat.cpm, From a3b9ef4af28008fc3ab66a22a52463f6e8690d1b Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 20 Nov 2023 10:29:01 +0100 Subject: [PATCH 05/10] Add better validation for the request --- modules/conceptxBidAdapter.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index 127b049bc99..d81249098e3 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -10,7 +10,7 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], isBidRequestValid: function (bid) { - return !!(bid.bidId); + return !!(bid.bidId && bid.params.site && bid.params.adunit ); }, buildRequests: function (validBidRequests, bidderRequest) { @@ -51,6 +51,9 @@ export const spec = { return bidResponses } const firstBid = bidResponsesFromServer[0] + if(!firstBid){ + return bidResponses + } const firstSeat = firstBid.ads[0] const bidResponse = { requestId: firstSeat.requestId, From 07d55acaed7542e299a3fdbd142565e79d3be88e Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 20 Nov 2023 10:32:34 +0100 Subject: [PATCH 06/10] Merge branch 'master' of https://github.com/prebid/Prebid.js --- .devcontainer/devcontainer.json | 4 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/issue_tracker.yml | 2 +- PR_REVIEW.md | 1 - features.json | 3 +- integrationExamples/gpt/adUnitFloors.html | 1 - .../gpt/adnuntius_example.html | 95 + integrationExamples/gpt/growthcode.html | 15 +- integrationExamples/gpt/hello_world.html | 30 +- integrationExamples/gpt/inskin_example.html | 100 - .../gpt/pbjs_video_adUnit.html | 109 - .../gpt/prebidServer_example.html | 40 +- .../gpt/prebidServer_fledge_example.html | 2 +- .../gpt/prebidServer_native_example.html | 2 +- integrationExamples/gpt/tpmn_example.html | 168 ++ .../gpt/tpmn_serverless_example.html | 121 + .../noadserver/native_noadserver.html | 173 ++ integrationExamples/topics/topics-server.js | 72 + .../videoModule/jwplayer/bidMarkedAsUsed.html | 4 +- .../jwplayer/bidRequestScheduling.html | 2 +- .../jwplayer/bidsBackHandlerOverride.html | 137 + .../videoModule/jwplayer/eventListeners.html | 6 +- .../videoModule/jwplayer/eventsUI.html | 4 +- .../jwplayer/gamAdServerMediation.html | 4 +- .../videoModule/jwplayer/mediaMetadata.html | 4 +- .../videoModule/jwplayer/playlist.html | 4 +- .../videoModule/videojs/bidMarkedAsUsed.html | 2 +- .../videojs/bidRequestScheduling.html | 2 +- .../videojs/bidsBackHandlerOverride.html | 164 ++ .../videoModule/videojs/eventListeners.html | 2 +- .../videoModule/videojs/eventsUI.html | 2 +- .../videojs/gamAdServerMediation.html | 2 +- .../videoModule/videojs/mediaMetadata.html | 2 +- .../videoModule/videojs/playlist.html | 2 +- .../anKeywords.js | 15 +- libraries/appnexusUtils/anUtils.js | 25 + libraries/chunk/chunk.js | 19 + libraries/cmp/cmpClient.js | 169 ++ libraries/currencyUtils/currency.js | 31 + libraries/gptUtils/gptUtils.js | 37 + libraries/htmlEscape/htmlEscape.js | 26 + libraries/mspa/activityControls.js | 127 + libraries/sizeUtils/sizeUtils.js | 29 + .../transformParamsUtils/convertTypes.js | 36 + libraries/uid2Eids/uid2Eids.js | 14 + libraries/urlUtils/urlUtils.js | 7 + modules/.submodules.json | 23 +- modules/33acrossAnalyticsAdapter.js | 656 +++++ modules/33acrossAnalyticsAdapter.md | 76 + modules/33acrossBidAdapter.js | 40 +- modules/33acrossIdSystem.js | 28 +- modules/a1MediaBidAdapter.js | 104 + modules/a1MediaBidAdapter.md | 93 + modules/a1MediaRtdProvider.js | 92 + modules/a1MediaRtdProvider.md | 46 + ...dsBidAdapter.js => acuityadsBidAdapter.js} | 9 + ...dsBidAdapter.md => acuityadsBidAdapter.md} | 2 +- modules/adWMGBidAdapter.js | 2 +- modules/adagioAnalyticsAdapter.js | 354 ++- modules/adagioAnalyticsAdapter.md | 10 +- modules/adagioBidAdapter.js | 52 +- modules/adagioBidAdapter.md | 7 +- modules/adfBidAdapter.js | 5 +- modules/adfusionBidAdapter.js | 90 + modules/adfusionBidAdapter.md | 61 + modules/adgenerationBidAdapter.js | 13 +- modules/adkernelAdnBidAdapter.js | 2 +- modules/adkernelBidAdapter.js | 74 +- modules/adlooxAnalyticsAdapter.js | 2 +- modules/adlooxRtdProvider.js | 2 +- modules/admanBidAdapter.js | 13 +- modules/admaticBidAdapter.js | 95 +- modules/admixerBidAdapter.js | 1 + modules/admixerIdSystem.js | 6 + modules/adnuntiusBidAdapter.js | 34 +- modules/adpod.js | 22 +- modules/adqueryBidAdapter.js | 48 +- modules/adqueryIdSystem.js | 75 +- modules/adrelevantisBidAdapter.js | 10 +- modules/adriverBidAdapter.js | 2 +- modules/adsinteractiveBidAdapter.js | 6 +- modules/adstirBidAdapter.js | 91 + modules/adstirBidAdapter.md | 36 + modules/adtargetBidAdapter.js | 3 +- modules/adtelligentBidAdapter.js | 28 +- modules/adtelligentIdSystem.js | 6 + modules/aduptechBidAdapter.js | 3 +- modules/adxcgBidAdapter.js | 413 +-- modules/adyoulikeBidAdapter.js | 54 +- modules/agmaAnalyticsAdapter.js | 225 ++ modules/agmaAnalyticsAdapter.md | 28 + modules/aidemBidAdapter.js | 371 +-- modules/ajaBidAdapter.js | 3 +- modules/alkimiBidAdapter.js | 54 +- modules/amxBidAdapter.js | 23 +- modules/amxIdSystem.js | 6 + modules/apacdexBidAdapter.js | 4 +- modules/appnexusBidAdapter.js | 21 +- modules/asoBidAdapter.js | 4 +- modules/asteriobidAnalyticsAdapter.js | 336 +++ modules/asteriobidAnalyticsAdapter.md | 41 + modules/audiencerunBidAdapter.js | 3 +- modules/automatadAnalyticsAdapter.js | 325 +++ modules/automatadAnalyticsAdapter.md | 23 + modules/axisBidAdapter.js | 210 ++ modules/axisBidAdapter.md | 83 + modules/beachfrontBidAdapter.js | 38 +- modules/beopBidAdapter.js | 19 +- modules/beopBidAdapter.md | 36 +- modules/betweenBidAdapter.js | 3 +- modules/bidViewability.js | 7 + modules/bidViewability.md | 17 +- modules/bidglassBidAdapter.js | 2 +- modules/bliinkBidAdapter.js | 111 +- modules/boldwinBidAdapter.js | 9 + modules/brandmetricsRtdProvider.js | 95 +- modules/brandmetricsRtdProvider.md | 19 +- modules/bridBidAdapter.js | 223 ++ modules/brightcomBidAdapter.js | 15 +- modules/brightcomSSPBidAdapter.js | 3 +- modules/britepoolIdSystem.js | 6 + modules/bucksenseBidAdapter.js | 1 + modules/cadentApertureMXBidAdapter.js | 34 +- modules/cadentApertureMXBidAdapter.md | 4 +- modules/categoryTranslation.js | 5 +- modules/coinzillaBidAdapter.js | 2 +- modules/concertBidAdapter.js | 10 + modules/connatixBidAdapter.js | 184 ++ modules/connatixBidAdapter.md | 54 + modules/connectIdSystem.js | 42 +- modules/connectadBidAdapter.js | 4 +- modules/consentManagement.js | 98 +- modules/consentManagementGpp.js | 462 ++-- modules/consentManagementUsp.js | 116 +- modules/consumableBidAdapter.js | 21 +- modules/conversantBidAdapter.js | 5 +- modules/cpmstarBidAdapter.js | 12 +- modules/craftBidAdapter.js | 6 +- modules/criteoBidAdapter.js | 191 +- modules/criteoIdSystem.js | 59 +- modules/currency.js | 112 +- modules/cwireBidAdapter.js | 19 + modules/czechAdIdSystem.js | 6 + modules/dacIdSystem.js | 6 + modules/datablocksBidAdapter.js | 3 +- modules/datawrkzBidAdapter.js | 10 +- modules/dchain.js | 4 +- modules/deepintentDpesIdSystem.js | 9 +- modules/dfpAdServerVideo.js | 6 +- modules/dgkeywordRtdProvider.js | 48 +- modules/discoveryBidAdapter.js | 66 +- modules/displayioBidAdapter.js | 4 +- modules/dmdIdSystem.js | 6 + modules/docereeAdManagerBidAdapter.js | 128 + modules/docereeAdManagerBidAdapter.md | 68 + modules/docereeBidAdapter.js | 31 +- modules/dxkultureBidAdapter.js | 278 +++ ...iaBidAdapter.md => dxkultureBidAdapter.md} | 20 +- modules/dynamicAdBoostRtdProvider.js | 114 + modules/dynamicAdBoostRtdProvider.md | 40 + modules/ebdrBidAdapter.js | 2 +- modules/edge226BidAdapter.js | 188 ++ modules/edge226BidAdapter.md | 79 + modules/eplanningBidAdapter.js | 22 +- modules/eskimiBidAdapter.js | 9 +- modules/euidIdSystem.js | 9 + modules/experianRtdProvider.js | 135 + modules/experianRtdProvider.md | 52 + modules/fabrickIdSystem.js | 6 + modules/fledgeForGpt.js | 99 +- modules/fledgeForGpt.md | 27 +- modules/flippBidAdapter.js | 186 ++ modules/flippBidAdapter.md | 45 + modules/fluctBidAdapter.js | 23 +- modules/freepassBidAdapter.js | 117 + modules/freepassBidAdapter.md | 34 + modules/freepassIdSystem.js | 28 +- modules/freewheel-sspBidAdapter.js | 14 +- modules/ftrackIdSystem.js | 16 + modules/gdprEnforcement.js | 246 +- modules/geoedgeRtdProvider.js | 62 +- modules/geoedgeRtdProvider.md | 3 +- modules/geolocationRtdProvider.js | 63 + modules/getintentBidAdapter.js | 2 +- modules/gmosspBidAdapter.js | 7 +- modules/goldbachBidAdapter.js | 19 +- modules/goldfishAdsRtdProvider.js | 194 ++ modules/goldfishAdsRtdProvider.md | 48 + modules/gppControl_usnat.js | 11 + modules/gppControl_usstates.js | 176 ++ modules/gptPreAuction.js | 28 +- modules/gravitoIdSystem.js | 6 + modules/greenbidsAnalyticsAdapter.js | 1 + modules/greenbidsRtdProvider.js | 2 +- modules/gridBidAdapter.js | 45 +- modules/growadvertisingBidAdapter.js | 2 +- modules/growthCodeAnalyticsAdapter.js | 20 +- modules/growthCodeAnalyticsAdapter.md | 8 +- modules/growthCodeIdSystem.js | 22 +- modules/growthCodeRtdProvider.js | 3 +- modules/gumgumBidAdapter.js | 39 +- modules/hadronIdSystem.js | 6 + modules/hadronIdSystem.md | 3 +- modules/holidBidAdapter.js | 78 +- modules/iasRtdProvider.js | 16 +- modules/id5IdSystem.js | 42 +- modules/id5IdSystem.md | 11 +- modules/identityLinkIdSystem.js | 22 +- modules/idxIdSystem.js | 6 + modules/illuminBidAdapter.js | 336 +++ modules/illuminBidAdapter.md | 35 + modules/imdsBidAdapter.js | 54 +- modules/imdsBidAdapter.md | 8 +- modules/impactifyBidAdapter.js | 212 +- modules/impactifyBidAdapter.md | 38 +- modules/improvedigitalBidAdapter.js | 4 + modules/imuIdSystem.js | 10 + modules/insticatorBidAdapter.js | 8 +- modules/integr8BidAdapter.js | 16 +- modules/integr8BidAdapter.md | 12 +- modules/intentIqIdSystem.js | 17 +- modules/invibesBidAdapter.js | 10 +- modules/ivsBidAdapter.js | 2 +- modules/ixBidAdapter.js | 592 ++++- modules/ixBidAdapter.md | 5 + modules/jixieBidAdapter.js | 151 +- modules/justIdSystem.js | 6 + modules/kargoBidAdapter.js | 43 +- modules/kinessoIdSystem.js | 9 +- modules/kueezBidAdapter.js | 14 +- modules/kueezRtbBidAdapter.js | 11 +- modules/kulturemediaBidAdapter.js | 472 ---- modules/liveIntentIdSystem.js | 128 +- modules/livewrappedAnalyticsAdapter.js | 66 +- modules/lm_kiviadsBidAdapter.js | 207 ++ modules/lm_kiviadsBidAdapter.md | 54 + modules/lockerdomeBidAdapter.js | 2 +- modules/lotamePanoramaIdSystem.js | 6 + modules/magniteAnalyticsAdapter.js | 4 + modules/mediabramaBidAdapter.js | 155 ++ modules/mediabramaBidAdapter.md | 33 + modules/mediafilterRtdProvider.js | 94 + modules/mediafilterRtdProvider.md | 37 + modules/mediafuseBidAdapter.js | 15 +- modules/mediagoBidAdapter.js | 12 +- modules/medianetBidAdapter.js | 13 +- modules/mediasniperBidAdapter.js | 3 +- modules/mediasquareBidAdapter.js | 45 +- modules/merkleIdSystem.js | 24 + modules/mgidBidAdapter.js | 3 +- modules/minutemediaBidAdapter.js | 33 +- modules/minutemediaplusBidAdapter.js | 11 +- modules/missenaBidAdapter.js | 42 +- modules/mobfoxpbBidAdapter.js | 9 + modules/mwOpenLinkIdSystem.js | 6 + modules/nativoBidAdapter.js | 24 +- modules/naveggIdSystem.js | 6 + modules/netIdSystem.js | 6 + modules/neuwoRtdProvider.js | 18 +- modules/neuwoRtdProvider.md | 2 + modules/newspassid.md | 76 + modules/newspassidBidAdapter.js | 62 +- modules/nextMillenniumBidAdapter.js | 3 +- modules/nextrollBidAdapter.js | 3 +- modules/nexx360BidAdapter.js | 12 +- modules/nobidAnalyticsAdapter.js | 199 ++ modules/nobidAnalyticsAdapter.md | 38 + modules/nobidBidAdapter.js | 34 +- modules/nobidBidAdapter.md | 1 + modules/novatiqIdSystem.js | 12 + modules/oguryBidAdapter.js | 55 +- modules/oneKeyIdSystem.js | 26 + modules/onetagBidAdapter.js | 8 +- modules/onomagicBidAdapter.js | 3 +- modules/open8BidAdapter.js | 3 +- modules/openwebBidAdapter.js | 3 +- modules/openxBidAdapter.js | 7 +- modules/operaadsBidAdapter.js | 47 +- modules/operaadsBidAdapter.md | 66 +- modules/operaadsIdSystem.js | 106 + modules/operaadsIdSystem.md | 52 + modules/optidigitalBidAdapter.js | 60 +- modules/optidigitalBidAdapter.md | 13 +- modules/optimeraRtdProvider.js | 26 +- modules/optimeraRtdProvider.md | 6 +- modules/orbidderBidAdapter.js | 18 +- modules/orbitsoftBidAdapter.js | 13 +- modules/otmBidAdapter.js | 3 +- modules/outbrainBidAdapter.js | 22 +- modules/oxxionAnalyticsAdapter.js | 79 +- modules/oxxionRtdProvider.js | 186 +- modules/oxxionRtdProvider.md | 20 +- modules/ozoneBidAdapter.js | 89 +- modules/ozoneBidAdapter.md | 4 +- modules/pairIdSystem.js | 6 + modules/pangleBidAdapter.js | 165 ++ modules/pangleBidAdapter.md | 32 + modules/parrableIdSystem.js | 40 +- modules/pgamsspBidAdapter.js | 212 ++ modules/pgamsspBidAdapter.md | 79 + modules/pixfutureBidAdapter.js | 5 +- modules/prebidServerBidAdapter/config.js | 24 +- modules/prebidServerBidAdapter/index.js | 28 +- .../prebidServerBidAdapter/ortbConverter.js | 27 +- modules/precisoBidAdapter.js | 212 ++ modules/precisoBidAdapter.md | 84 + modules/priceFloors.js | 63 +- modules/prismaBidAdapter.js | 2 +- modules/programmaticaBidAdapter.js | 153 ++ modules/programmaticaBidAdapter.md | 46 + modules/pubCircleBidAdapter.js | 231 ++ modules/pubCircleBidAdapter.md | 79 + modules/publinkIdSystem.js | 50 +- modules/pubmaticAnalyticsAdapter.js | 55 +- modules/pubmaticBidAdapter.js | 71 +- modules/pubxaiAnalyticsAdapter.js | 3 +- modules/pulsepointBidAdapter.js | 488 +--- modules/pulsepointBidAdapter.md | 84 +- modules/qortexRtdProvider.js | 165 ++ modules/qortexRtdProvider.md | 69 + modules/quantcastIdSystem.js | 6 + modules/rasBidAdapter.js | 55 +- modules/relaidoBidAdapter.js | 12 +- modules/relayBidAdapter.js | 99 + modules/relayBidAdapter.md | 79 + modules/relevadRtdProvider.js | 13 +- modules/relevantdigitalBidAdapter.js | 27 +- modules/resetdigitalBidAdapter.js | 122 +- modules/revcontentBidAdapter.js | 3 +- modules/riseBidAdapter.js | 14 +- modules/rubiconBidAdapter.js | 95 +- modules/schain.js | 27 +- modules/sharedIdSystem.js | 8 +- modules/sharethroughAnalyticsAdapter.js | 2 +- modules/sharethroughBidAdapter.js | 153 +- modules/shinezBidAdapter.js | 14 +- modules/showheroes-bsBidAdapter.js | 6 +- modules/silverpushBidAdapter.js | 4 +- modules/slimcutBidAdapter.js | 2 +- modules/smaatoBidAdapter.js | 22 +- modules/smartxBidAdapter.js | 15 +- modules/smartxBidAdapter.md | 4 +- modules/smartyadsBidAdapter.js | 70 +- modules/smartyadsBidAdapter.md | 18 +- modules/smilewantedBidAdapter.js | 3 + modules/snigelBidAdapter.js | 87 +- modules/snigelBidAdapter.md | 12 +- modules/sonobiAnalyticsAdapter.js | 2 +- modules/sonobiBidAdapter.js | 35 +- modules/sovrnBidAdapter.js | 15 +- modules/sparteoBidAdapter.js | 126 + modules/sparteoBidAdapter.md | 35 + modules/spotxBidAdapter.js | 18 +- modules/ssmasBidAdapter.js | 133 + modules/ssmasBidAdapter.md | 35 + modules/sspBCBidAdapter.js | 68 +- modules/stroeerCoreBidAdapter.js | 5 +- modules/taboolaBidAdapter.js | 2 +- modules/tapadIdSystem.js | 6 + modules/tappxBidAdapter.js | 48 +- modules/tappxBidAdapter.md | 17 + modules/teadsBidAdapter.js | 32 +- modules/tncIdSystem.js | 6 + modules/topicsFpdModule.js | 122 +- modules/topicsFpdModule.md | 4 + modules/tpmnBidAdapter.js | 346 ++- modules/tpmnBidAdapter.md | 86 +- modules/trafficgateBidAdapter.js | 199 +- modules/trionBidAdapter.js | 3 +- modules/tripleliftBidAdapter.js | 60 +- modules/ttdBidAdapter.js | 4 +- modules/uid2IdSystem.js | 25 +- modules/uid2IdSystem.md | 131 +- modules/uid2IdSystem_shared.js | 599 ++++- modules/underdogmediaBidAdapter.js | 4 +- modules/undertoneBidAdapter.js | 1 + modules/undertoneBidAdapter.md | 2 +- modules/unifiedIdSystem.js | 11 + modules/userId/eids.js | 459 +--- modules/userId/eids.md | 28 +- modules/userId/index.js | 256 +- modules/userId/userId.md | 3 + modules/utiqSystem.js | 9 + modules/vdoaiBidAdapter.js | 2 +- modules/ventesBidAdapter.js | 3 +- modules/verizonMediaIdSystem.js | 6 + modules/viantOrtbBidAdapter.js | 113 + modules/viantOrtbBidAdapter.md | 54 + modules/vidazooBidAdapter.js | 100 +- modules/videojsVideoProvider.js | 11 +- modules/videoreachBidAdapter.js | 2 +- modules/visxBidAdapter.js | 64 +- modules/voxBidAdapter.js | 21 +- modules/vrtcalBidAdapter.js | 45 +- modules/weboramaRtdProvider.js | 4 +- modules/winrBidAdapter.js | 6 +- modules/xeBidAdapter.js | 3 +- modules/yahoosspBidAdapter.js | 71 +- modules/yahoosspBidAdapter.md | 201 +- modules/yandexBidAdapter.js | 56 +- modules/yieldloveBidAdapter.js | 149 ++ modules/yieldloveBidAdapter.md | 38 + modules/yieldmoBidAdapter.js | 23 +- modules/zeotapIdPlusIdSystem.js | 6 + modules/zeta_global_sspAnalyticsAdapter.js | 77 +- modules/zeta_global_sspBidAdapter.js | 7 +- package-lock.json | 408 +-- package.json | 6 +- src/activities/params.js | 5 +- src/activities/redactor.js | 12 +- src/adapterManager.js | 64 +- src/adapters/bidderFactory.js | 47 +- src/adloader.js | 6 +- src/ajax.js | 221 +- src/auction.js | 154 +- src/auctionManager.js | 121 +- src/bidTTL.js | 25 + src/config.js | 19 +- src/consentHandler.js | 74 +- src/constants.json | 16 +- src/events.js | 83 +- src/fpd/sua.js | 10 +- src/native.js | 22 +- src/prebid.js | 36 +- src/targeting.js | 21 +- src/utils.js | 471 +--- src/utils/currency.js | 16 - src/utils/reducers.js | 44 + src/utils/ttlCollection.js | 139 ++ src/video.js | 30 +- test/mocks/xhr.js | 265 +- test/spec/appnexusKeywords_spec.js | 2 +- test/spec/auctionmanager_spec.js | 485 ++-- test/spec/fpd/enrichment_spec.js | 8 +- test/spec/fpd/sua_spec.js | 12 + test/spec/libraries/cmp/cmpClient_spec.js | 296 +++ test/spec/libraries/currencyUtils_spec.js | 113 + .../libraries/mspa/activityControls_spec.js | 256 ++ test/spec/libraries/sizeUtils_spec.js | 30 + test/spec/libraries/urlUtils_spec.js | 24 + .../modules/33acrossAnalyticsAdapter_spec.js | 1163 +++++++++ test/spec/modules/33acrossBidAdapter_spec.js | 223 +- test/spec/modules/33acrossIdSystem_spec.js | 149 +- test/spec/modules/a1MediaBidAdapter_spec.js | 248 ++ test/spec/modules/a1MediaRtdProvider_spec.js | 105 + test/spec/modules/acuityadsBidAdapter_spec.js | 428 ++++ .../modules/adagioAnalyticsAdapter_spec.js | 602 ++++- test/spec/modules/adagioBidAdapter_spec.js | 108 +- test/spec/modules/adfBidAdapter_spec.js | 47 + test/spec/modules/adfusionBidAdapter_spec.js | 97 + .../modules/adgenerationBidAdapter_spec.js | 12 +- .../modules/adkernelAdnBidAdapter_spec.js | 3 +- test/spec/modules/adkernelBidAdapter_spec.js | 35 +- test/spec/modules/adlooxAdServerVideo_spec.js | 6 +- test/spec/modules/adlooxRtdProvider_spec.js | 6 +- test/spec/modules/admanBidAdapter_spec.js | 10 +- test/spec/modules/admaticBidAdapter_spec.js | 163 ++ test/spec/modules/adnuntiusBidAdapter_spec.js | 202 +- test/spec/modules/adpod_spec.js | 3 - test/spec/modules/adqueryBidAdapter_spec.js | 6 +- test/spec/modules/adqueryIdSystem_spec.js | 39 +- test/spec/modules/adstirBidAdapter_spec.js | 412 +++ .../modules/adtelligentBidAdapter_spec.js | 17 +- test/spec/modules/adxcgBidAdapter_spec.js | 1372 ++++------ test/spec/modules/adyoulikeBidAdapter_spec.js | 149 +- .../spec/modules/agmaAnalyticsAdapter_spec.js | 388 +++ test/spec/modules/aidemBidAdapter_spec.js | 483 ++-- test/spec/modules/alkimiBidAdapter_spec.js | 29 +- test/spec/modules/amxBidAdapter_spec.js | 9 +- test/spec/modules/appnexusBidAdapter_spec.js | 13 +- .../asteriobidAnalyticsAdapter_spec.js | 151 ++ .../modules/automatadAnalyticsAdapter_spec.js | 533 ++++ test/spec/modules/axisBidAdapter_spec.js | 414 +++ .../spec/modules/beachfrontBidAdapter_spec.js | 37 +- test/spec/modules/beopBidAdapter_spec.js | 20 +- test/spec/modules/bidViewability_spec.js | 33 +- test/spec/modules/bliinkBidAdapter_spec.js | 885 +++++-- test/spec/modules/boldwinBidAdapter_spec.js | 33 +- .../modules/brandmetricsRtdProvider_spec.js | 82 +- test/spec/modules/bridBidAdapter_spec.js | 129 + .../modules/byDataAnalyticsAdapter_spec.js | 16 +- .../cadentApertureMXBidAdapter_spec.js | 516 ++-- test/spec/modules/categoryTranslation_spec.js | 16 +- test/spec/modules/chtnwBidAdapter_spec.js | 5 +- test/spec/modules/concertBidAdapter_spec.js | 42 +- test/spec/modules/connatixBidAdapter_spec.js | 350 +++ test/spec/modules/connectIdSystem_spec.js | 74 + .../spec/modules/consentManagementGpp_spec.js | 924 ++++--- .../spec/modules/consentManagementUsp_spec.js | 13 + test/spec/modules/consentManagement_spec.js | 26 +- .../spec/modules/consumableBidAdapter_spec.js | 72 +- .../conversantAnalyticsAdapter_spec.js | 8 +- test/spec/modules/criteoBidAdapter_spec.js | 456 +++- test/spec/modules/criteoIdSystem_spec.js | 69 +- test/spec/modules/currency_spec.js | 116 +- test/spec/modules/cwireBidAdapter_spec.js | 47 + .../spec/modules/datablocksBidAdapter_spec.js | 10 +- test/spec/modules/dfpAdServerVideo_spec.js | 14 +- .../spec/modules/dgkeywordRtdProvider_spec.js | 34 +- test/spec/modules/discoveryBidAdapter_spec.js | 47 + test/spec/modules/dmdIdSystem_spec.js | 4 +- .../docereeAdManagerBidAdapter_spec.js | 125 + test/spec/modules/docereeBidAdapter_spec.js | 33 + test/spec/modules/dxkultureBidAdapter_spec.js | 682 +++++ .../modules/dynamicAdBoostRtdProvider_spec.js | 77 + test/spec/modules/edge226BidAdapter_spec.js | 373 +++ test/spec/modules/eids_spec.js | 201 ++ test/spec/modules/eplanningBidAdapter_spec.js | 67 + test/spec/modules/euidIdSystem_spec.js | 19 +- test/spec/modules/experianRtdProvider_spec.js | 365 +++ test/spec/modules/feedadBidAdapter_spec.js | 2 +- test/spec/modules/fledgeForGpt_spec.js | 419 ++++ test/spec/modules/fledge_spec.js | 208 -- test/spec/modules/flippBidAdapter_spec.js | 170 ++ test/spec/modules/fluctBidAdapter_spec.js | 159 +- test/spec/modules/freepassBidAdapter_spec.js | 205 ++ test/spec/modules/freepassIdSystem_spec.js | 87 +- .../modules/freewheel-sspBidAdapter_spec.js | 20 +- test/spec/modules/gdprEnforcement_spec.js | 580 +++-- test/spec/modules/geoedgeRtdProvider_spec.js | 91 +- .../modules/geolocationRtdProvider_spec.js | 110 + .../modules/goldfishAdsRtdProvider_spec.js | 163 ++ test/spec/modules/gppControl_usstates_spec.js | 519 ++++ test/spec/modules/gptPreAuction_spec.js | 12 + .../modules/greenbidsAnalyticsAdapter_spec.js | 15 + .../spec/modules/greenbidsRtdProvider_spec.js | 17 +- test/spec/modules/gridBidAdapter_spec.js | 185 +- .../growthCodeAnalyticsAdapter_spec.js | 1 - test/spec/modules/gumgumBidAdapter_spec.js | 60 +- test/spec/modules/hadronIdSystem_spec.js | 2 +- test/spec/modules/holidBidAdapter_spec.js | 32 +- test/spec/modules/id5AnalyticsAdapter_spec.js | 8 +- test/spec/modules/id5IdSystem_spec.js | 70 +- .../spec/modules/identityLinkIdSystem_spec.js | 55 +- test/spec/modules/illuminBidAdapter_spec.js | 634 +++++ test/spec/modules/imdsBidAdapter_spec.js | 115 +- test/spec/modules/impactifyBidAdapter_spec.js | 173 +- .../modules/improvedigitalBidAdapter_spec.js | 10 + .../spec/modules/insticatorBidAdapter_spec.js | 8 +- test/spec/modules/integr8BidAdapter_spec.js | 4 +- test/spec/modules/intentIqIdSystem_spec.js | 40 +- test/spec/modules/invibesBidAdapter_spec.js | 155 +- .../modules/invisiblyAnalyticsAdapter_spec.js | 8 +- test/spec/modules/ixBidAdapter_spec.js | 1348 +++++++++- test/spec/modules/jixieBidAdapter_spec.js | 243 +- test/spec/modules/jwplayerRtdProvider_spec.js | 54 +- test/spec/modules/kargoBidAdapter_spec.js | 278 ++- test/spec/modules/kueezRtbBidAdapter_spec.js | 21 +- .../modules/kulturemediaBidAdapter_spec.js | 612 ----- test/spec/modules/lassoBidAdapter_spec.js | 10 +- .../modules/liveIntentIdMinimalSystem_spec.js | 34 +- test/spec/modules/liveIntentIdSystem_spec.js | 55 +- .../spec/modules/lm_kiviadsBidAdapter_spec.js | 455 ++++ .../modules/magniteAnalyticsAdapter_spec.js | 38 +- .../spec/modules/mediabramaBidAdapter_spec.js | 256 ++ .../modules/mediafilterRtdProvider_spec.js | 147 ++ test/spec/modules/mediagoBidAdapter_spec.js | 29 + test/spec/modules/medianetBidAdapter_spec.js | 153 +- .../modules/mediasquareBidAdapter_spec.js | 11 + test/spec/modules/mgidRtdProvider_spec.js | 6 +- .../modules/minutemediaBidAdapter_spec.js | 41 +- .../modules/minutemediaplusBidAdapter_spec.js | 19 + test/spec/modules/missenaBidAdapter_spec.js | 44 +- test/spec/modules/mobfoxpbBidAdapter_spec.js | 33 +- test/spec/modules/multibid_spec.js | 21 +- test/spec/modules/nativoBidAdapter_spec.js | 7 +- .../spec/modules/newspassidBidAdapter_spec.js | 6 + test/spec/modules/nexx360BidAdapter_spec.js | 51 +- .../modules/nobidAnalyticsAdapter_spec.js | 494 ++++ test/spec/modules/nobidBidAdapter_spec.js | 57 +- test/spec/modules/oguryBidAdapter_spec.js | 426 +++- test/spec/modules/onetagBidAdapter_spec.js | 245 +- .../spec/modules/ooloAnalyticsAdapter_spec.js | 2 +- test/spec/modules/openxBidAdapter_spec.js | 2 + test/spec/modules/operaadsBidAdapter_spec.js | 89 + test/spec/modules/operaadsIdSystem_spec.js | 53 + .../modules/optidigitalBidAdapter_spec.js | 33 +- test/spec/modules/optimeraRtdProvider_spec.js | 71 +- test/spec/modules/orbidderBidAdapter_spec.js | 149 +- test/spec/modules/outbrainBidAdapter_spec.js | 63 +- .../modules/oxxionAnalyticsAdapter_spec.js | 51 +- test/spec/modules/oxxionRtdProvider_spec.js | 65 +- test/spec/modules/ozoneBidAdapter_spec.js | 41 +- test/spec/modules/pangleBidAdapter_spec.js | 295 +++ ...pter_spec.js => pgamsspBidAdapter_spec.js} | 12 +- .../modules/prebidServerBidAdapter_spec.js | 631 ++--- test/spec/modules/precisoBidAdapter_spec.js | 162 ++ test/spec/modules/priceFloors_spec.js | 273 +- .../modules/programmaticaBidAdapter_spec.js | 263 ++ test/spec/modules/pubCircleBidAdapter_spec.js | 399 +++ test/spec/modules/publinkIdSystem_spec.js | 45 +- .../modules/pubmaticAnalyticsAdapter_spec.js | 236 +- test/spec/modules/pubmaticBidAdapter_spec.js | 771 +++--- .../modules/pubwiseAnalyticsAdapter_spec.js | 10 +- .../modules/pubxaiAnalyticsAdapter_spec.js | 16 +- .../spec/modules/pulsepointBidAdapter_spec.js | 711 ++---- test/spec/modules/qortexRtdProvider_spec.js | 333 +++ test/spec/modules/rasBidAdapter_spec.js | 52 + test/spec/modules/relaidoBidAdapter_spec.js | 7 +- test/spec/modules/relayBidAdapter_spec.js | 131 + .../modules/relevantdigitalBidAdapter_spec.js | 92 +- test/spec/modules/rubiconBidAdapter_spec.js | 498 +++- .../modules/sharethroughBidAdapter_spec.js | 295 ++- test/spec/modules/smaatoBidAdapter_spec.js | 10 +- test/spec/modules/smartyadsBidAdapter_spec.js | 99 +- test/spec/modules/snigelBidAdapter_spec.js | 64 +- .../modules/sonobiAnalyticsAdapter_spec.js | 6 +- test/spec/modules/sonobiBidAdapter_spec.js | 23 +- test/spec/modules/sovrnBidAdapter_spec.js | 85 +- test/spec/modules/sparteoBidAdapter_spec.js | 408 +++ test/spec/modules/ssmasBidAdapter_spec.js | 244 ++ test/spec/modules/sspBCBidAdapter_spec.js | 37 +- .../modules/stroeerCoreBidAdapter_spec.js | 13 +- test/spec/modules/taboolaBidAdapter_spec.js | 2 +- test/spec/modules/tappxBidAdapter_spec.js | 19 + test/spec/modules/teadsBidAdapter_spec.js | 209 +- test/spec/modules/teadsIdSystem_spec.js | 2 +- test/spec/modules/topicsFpdModule_spec.js | 777 +++--- test/spec/modules/tpmnBidAdapter_spec.js | 511 +++- .../modules/trafficgateBidAdapter_spec.js | 1548 ++++++++++-- .../spec/modules/tripleliftBidAdapter_spec.js | 112 +- test/spec/modules/ttdBidAdapter_spec.js | 5 + test/spec/modules/ucfunnelBidAdapter_spec.js | 53 +- test/spec/modules/uid2IdSystem_helpers.js | 14 +- test/spec/modules/uid2IdSystem_spec.js | 369 ++- .../modules/underdogmediaBidAdapter_spec.js | 15 + test/spec/modules/undertoneBidAdapter_spec.js | 38 +- test/spec/modules/userId_spec.js | 639 ++++- test/spec/modules/viantOrtbBidAdapter_spec.js | 432 ++++ test/spec/modules/vidazooBidAdapter_spec.js | 134 +- test/spec/modules/visxBidAdapter_spec.js | 152 +- test/spec/modules/voxBidAdapter_spec.js | 93 + test/spec/modules/vrtcalBidAdapter_spec.js | 51 +- test/spec/modules/xeBidAdapter_spec.js | 734 +++--- test/spec/modules/yahoosspBidAdapter_spec.js | 735 +++--- test/spec/modules/yandexBidAdapter_spec.js | 59 +- test/spec/modules/yieldloveBidAdapter_spec.js | 128 + test/spec/modules/yieldmoBidAdapter_spec.js | 35 +- .../zeta_global_sspAnalyticsAdapter_spec.js | 92 +- .../modules/zeta_global_sspBidAdapter_spec.js | 18 +- test/spec/native_spec.js | 25 +- test/spec/unit/core/adapterManager_spec.js | 12 +- test/spec/unit/core/ajax_spec.js | 415 +++ test/spec/unit/core/bidderFactory_spec.js | 2222 +++++++++-------- test/spec/unit/core/consentHandler_spec.js | 84 +- test/spec/unit/core/events_spec.js | 30 + test/spec/unit/core/targeting_spec.js | 42 +- test/spec/unit/pbjs_api_spec.js | 24 +- test/spec/unit/utils/reducers_spec.js | 124 + test/spec/unit/utils/ttlCollection_spec.js | 180 ++ test/spec/utils_spec.js | 165 +- test/spec/videoCache_spec.js | 8 +- test/spec/video_spec.js | 244 +- test/test_deps.js | 10 + 654 files changed, 52485 insertions(+), 15093 deletions(-) create mode 100644 integrationExamples/gpt/adnuntius_example.html delete mode 100644 integrationExamples/gpt/inskin_example.html delete mode 100644 integrationExamples/gpt/pbjs_video_adUnit.html create mode 100644 integrationExamples/gpt/tpmn_example.html create mode 100644 integrationExamples/gpt/tpmn_serverless_example.html create mode 100755 integrationExamples/noadserver/native_noadserver.html create mode 100644 integrationExamples/topics/topics-server.js create mode 100644 integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html create mode 100644 integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html rename libraries/{appnexusKeywords => appnexusUtils}/anKeywords.js (91%) create mode 100644 libraries/appnexusUtils/anUtils.js create mode 100644 libraries/chunk/chunk.js create mode 100644 libraries/cmp/cmpClient.js create mode 100644 libraries/currencyUtils/currency.js create mode 100644 libraries/gptUtils/gptUtils.js create mode 100644 libraries/htmlEscape/htmlEscape.js create mode 100644 libraries/mspa/activityControls.js create mode 100644 libraries/sizeUtils/sizeUtils.js create mode 100644 libraries/transformParamsUtils/convertTypes.js create mode 100644 libraries/uid2Eids/uid2Eids.js create mode 100644 libraries/urlUtils/urlUtils.js create mode 100644 modules/33acrossAnalyticsAdapter.js create mode 100644 modules/33acrossAnalyticsAdapter.md create mode 100644 modules/a1MediaBidAdapter.js create mode 100644 modules/a1MediaBidAdapter.md create mode 100644 modules/a1MediaRtdProvider.js create mode 100644 modules/a1MediaRtdProvider.md rename modules/{acuityAdsBidAdapter.js => acuityadsBidAdapter.js} (94%) rename modules/{acuityAdsBidAdapter.md => acuityadsBidAdapter.md} (98%) create mode 100644 modules/adfusionBidAdapter.js create mode 100644 modules/adfusionBidAdapter.md create mode 100644 modules/adstirBidAdapter.js create mode 100644 modules/adstirBidAdapter.md create mode 100644 modules/agmaAnalyticsAdapter.js create mode 100644 modules/agmaAnalyticsAdapter.md create mode 100644 modules/asteriobidAnalyticsAdapter.js create mode 100644 modules/asteriobidAnalyticsAdapter.md create mode 100644 modules/automatadAnalyticsAdapter.js create mode 100644 modules/automatadAnalyticsAdapter.md create mode 100644 modules/axisBidAdapter.js create mode 100644 modules/axisBidAdapter.md create mode 100644 modules/bridBidAdapter.js create mode 100644 modules/connatixBidAdapter.js create mode 100644 modules/connatixBidAdapter.md create mode 100644 modules/docereeAdManagerBidAdapter.js create mode 100644 modules/docereeAdManagerBidAdapter.md create mode 100644 modules/dxkultureBidAdapter.js rename modules/{kulturemediaBidAdapter.md => dxkultureBidAdapter.md} (86%) create mode 100644 modules/dynamicAdBoostRtdProvider.js create mode 100644 modules/dynamicAdBoostRtdProvider.md create mode 100644 modules/edge226BidAdapter.js create mode 100644 modules/edge226BidAdapter.md create mode 100644 modules/experianRtdProvider.js create mode 100644 modules/experianRtdProvider.md create mode 100644 modules/flippBidAdapter.js create mode 100644 modules/flippBidAdapter.md create mode 100644 modules/freepassBidAdapter.js create mode 100644 modules/freepassBidAdapter.md create mode 100644 modules/geolocationRtdProvider.js create mode 100755 modules/goldfishAdsRtdProvider.js create mode 100755 modules/goldfishAdsRtdProvider.md create mode 100644 modules/gppControl_usnat.js create mode 100644 modules/gppControl_usstates.js create mode 100644 modules/illuminBidAdapter.js create mode 100644 modules/illuminBidAdapter.md delete mode 100644 modules/kulturemediaBidAdapter.js create mode 100644 modules/lm_kiviadsBidAdapter.js create mode 100644 modules/lm_kiviadsBidAdapter.md create mode 100644 modules/mediabramaBidAdapter.js create mode 100644 modules/mediabramaBidAdapter.md create mode 100644 modules/mediafilterRtdProvider.js create mode 100644 modules/mediafilterRtdProvider.md create mode 100644 modules/newspassid.md create mode 100644 modules/nobidAnalyticsAdapter.js create mode 100644 modules/nobidAnalyticsAdapter.md create mode 100644 modules/operaadsIdSystem.js create mode 100644 modules/operaadsIdSystem.md create mode 100644 modules/pangleBidAdapter.js create mode 100644 modules/pangleBidAdapter.md create mode 100644 modules/pgamsspBidAdapter.js create mode 100644 modules/pgamsspBidAdapter.md create mode 100644 modules/precisoBidAdapter.js create mode 100644 modules/precisoBidAdapter.md create mode 100644 modules/programmaticaBidAdapter.js create mode 100644 modules/programmaticaBidAdapter.md create mode 100644 modules/pubCircleBidAdapter.js create mode 100644 modules/pubCircleBidAdapter.md create mode 100644 modules/qortexRtdProvider.js create mode 100644 modules/qortexRtdProvider.md create mode 100644 modules/relayBidAdapter.js create mode 100644 modules/relayBidAdapter.md create mode 100644 modules/sparteoBidAdapter.js create mode 100644 modules/sparteoBidAdapter.md create mode 100644 modules/ssmasBidAdapter.js create mode 100644 modules/ssmasBidAdapter.md create mode 100644 modules/viantOrtbBidAdapter.js create mode 100644 modules/viantOrtbBidAdapter.md create mode 100644 modules/yieldloveBidAdapter.js create mode 100644 modules/yieldloveBidAdapter.md create mode 100644 src/bidTTL.js delete mode 100644 src/utils/currency.js create mode 100644 src/utils/reducers.js create mode 100644 src/utils/ttlCollection.js create mode 100644 test/spec/libraries/cmp/cmpClient_spec.js create mode 100644 test/spec/libraries/currencyUtils_spec.js create mode 100644 test/spec/libraries/mspa/activityControls_spec.js create mode 100644 test/spec/libraries/sizeUtils_spec.js create mode 100644 test/spec/libraries/urlUtils_spec.js create mode 100644 test/spec/modules/33acrossAnalyticsAdapter_spec.js create mode 100644 test/spec/modules/a1MediaBidAdapter_spec.js create mode 100644 test/spec/modules/a1MediaRtdProvider_spec.js create mode 100644 test/spec/modules/acuityadsBidAdapter_spec.js create mode 100644 test/spec/modules/adfusionBidAdapter_spec.js create mode 100644 test/spec/modules/adstirBidAdapter_spec.js create mode 100644 test/spec/modules/agmaAnalyticsAdapter_spec.js create mode 100644 test/spec/modules/asteriobidAnalyticsAdapter_spec.js create mode 100644 test/spec/modules/automatadAnalyticsAdapter_spec.js create mode 100644 test/spec/modules/axisBidAdapter_spec.js create mode 100644 test/spec/modules/bridBidAdapter_spec.js create mode 100644 test/spec/modules/connatixBidAdapter_spec.js create mode 100644 test/spec/modules/docereeAdManagerBidAdapter_spec.js create mode 100644 test/spec/modules/dxkultureBidAdapter_spec.js create mode 100644 test/spec/modules/dynamicAdBoostRtdProvider_spec.js create mode 100644 test/spec/modules/edge226BidAdapter_spec.js create mode 100644 test/spec/modules/experianRtdProvider_spec.js create mode 100644 test/spec/modules/fledgeForGpt_spec.js delete mode 100644 test/spec/modules/fledge_spec.js create mode 100644 test/spec/modules/flippBidAdapter_spec.js create mode 100644 test/spec/modules/freepassBidAdapter_spec.js create mode 100644 test/spec/modules/geolocationRtdProvider_spec.js create mode 100755 test/spec/modules/goldfishAdsRtdProvider_spec.js create mode 100644 test/spec/modules/gppControl_usstates_spec.js create mode 100644 test/spec/modules/illuminBidAdapter_spec.js delete mode 100644 test/spec/modules/kulturemediaBidAdapter_spec.js create mode 100644 test/spec/modules/lm_kiviadsBidAdapter_spec.js create mode 100644 test/spec/modules/mediabramaBidAdapter_spec.js create mode 100644 test/spec/modules/mediafilterRtdProvider_spec.js create mode 100644 test/spec/modules/nobidAnalyticsAdapter_spec.js create mode 100644 test/spec/modules/operaadsIdSystem_spec.js create mode 100644 test/spec/modules/pangleBidAdapter_spec.js rename test/spec/modules/{acuityAdsBidAdapter_spec.js => pgamsspBidAdapter_spec.js} (96%) create mode 100644 test/spec/modules/precisoBidAdapter_spec.js create mode 100644 test/spec/modules/programmaticaBidAdapter_spec.js create mode 100644 test/spec/modules/pubCircleBidAdapter_spec.js create mode 100644 test/spec/modules/qortexRtdProvider_spec.js create mode 100644 test/spec/modules/relayBidAdapter_spec.js create mode 100644 test/spec/modules/sparteoBidAdapter_spec.js create mode 100644 test/spec/modules/ssmasBidAdapter_spec.js create mode 100644 test/spec/modules/viantOrtbBidAdapter_spec.js create mode 100644 test/spec/modules/yieldloveBidAdapter_spec.js create mode 100644 test/spec/unit/core/ajax_spec.js create mode 100644 test/spec/unit/core/events_spec.js create mode 100644 test/spec/unit/utils/reducers_spec.js create mode 100644 test/spec/unit/utils/ttlCollection_spec.js diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 104d9a38132..d4c34929569 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,7 +9,9 @@ }, "postCreateCommand": "bash .devcontainer/postCreate.sh", - + // Make is-docker work again + "postStartCommand": "test -f /.dockerenv || sudo touch /.dockerenv", + // Set *default* container specific settings.json values on container create. "settings": {}, diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 584f6f8894a..84c97376a3e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/issue_tracker.yml b/.github/workflows/issue_tracker.yml index 69cf4c5fc7f..b5c59c85160 100644 --- a/.github/workflows/issue_tracker.yml +++ b/.github/workflows/issue_tracker.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a with: app_id: ${{ secrets.ISSUE_APP_ID }} private_key: ${{ secrets.ISSUE_APP_PEM }} diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 45ca30a7a3d..9deac9963fb 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -55,7 +55,6 @@ Follow steps above for general review process. In addition, please verify the fo - Adapters that accept a floor parameter must also support the [floors module](https://docs.prebid.org/dev-docs/modules/floors.html) -- look for a call to the `getFloor()` function. - Adapters cannot accept an schain parameter. Rather, they must look for the schain parameter at bidRequest.schain. - The bidderRequest.refererInfo.referer must be checked in addition to any bidder-specific parameter. - - If they're getting the COPPA flag, it must come from config.getConfig('coppa'); - Page position must come from bidrequest.mediaTypes.banner.pos or bidrequest.mediaTypes.video.pos - Global OpenRTB fields should come from [getConfig('ortb2');](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-fpd): - bcat, battr, badv diff --git a/features.json b/features.json index ccb2166a05f..4d8377cda7d 100644 --- a/features.json +++ b/features.json @@ -1,4 +1,5 @@ [ "NATIVE", - "VIDEO" + "VIDEO", + "UID2_CSTG" ] diff --git a/integrationExamples/gpt/adUnitFloors.html b/integrationExamples/gpt/adUnitFloors.html index bb48a20ef78..a80e1b2380b 100644 --- a/integrationExamples/gpt/adUnitFloors.html +++ b/integrationExamples/gpt/adUnitFloors.html @@ -109,4 +109,3 @@
Div-1
- diff --git a/integrationExamples/gpt/adnuntius_example.html b/integrationExamples/gpt/adnuntius_example.html new file mode 100644 index 00000000000..b61c4e0674e --- /dev/null +++ b/integrationExamples/gpt/adnuntius_example.html @@ -0,0 +1,95 @@ + + + + + + + +

Adnuntius Prebid Adaptor Test

+
Ad Slot 1
+ + +
+ +
+ + + diff --git a/integrationExamples/gpt/growthcode.html b/integrationExamples/gpt/growthcode.html index d8ad6c4a5af..35de2b710ad 100644 --- a/integrationExamples/gpt/growthcode.html +++ b/integrationExamples/gpt/growthcode.html @@ -56,20 +56,11 @@ provider: 'growthCodeAnalytics', options: { pid: 'TEST01', + //url: 'http://localhost:8080/v3/pb/analytics', trackEvents: [ - 'auctionInit', 'auctionEnd', - 'bidAdjustment', - 'bidTimeout', - 'bidTimeout', - 'bidRequested', - 'bidResponse', - 'setTargeting', - 'requestBids', - 'addAdUnits', - 'noBid', 'bidWon', - 'bidderDone'] + ] } }); pbjs.setConfig({ @@ -80,7 +71,7 @@ auctionDelay: 1000, dataProviders: [{ name: 'growthCodeRtd', - waitForIt: true, + waitForIt: false, params: { pid: 'TEST01', } diff --git a/integrationExamples/gpt/hello_world.html b/integrationExamples/gpt/hello_world.html index 47ba5b8f18a..03a2356f0ef 100644 --- a/integrationExamples/gpt/hello_world.html +++ b/integrationExamples/gpt/hello_world.html @@ -8,6 +8,7 @@ --> + @@ -19,9 +20,10 @@ code: 'div-gpt-ad-1460505748561-0', mediaTypes: { banner: { - sizes: [[300, 250], [300,600]], + sizes: [[300, 250]], } }, + // Replace this object to test a new Adapter! bids: [{ bidder: 'appnexus', @@ -40,12 +42,13 @@ - +

Prebid.js Test

+
Div-1
+
+ +
+ \ No newline at end of file diff --git a/integrationExamples/gpt/inskin_example.html b/integrationExamples/gpt/inskin_example.html deleted file mode 100644 index 45fd448cf48..00000000000 --- a/integrationExamples/gpt/inskin_example.html +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - -

Prebid.js Test

-
Div-1
-
- -
- - diff --git a/integrationExamples/gpt/pbjs_video_adUnit.html b/integrationExamples/gpt/pbjs_video_adUnit.html deleted file mode 100644 index 080ca9be142..00000000000 --- a/integrationExamples/gpt/pbjs_video_adUnit.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - Prebid.js video adUnit example - - - - - - - - - - - -
- -
- - - - - diff --git a/integrationExamples/gpt/prebidServer_example.html b/integrationExamples/gpt/prebidServer_example.html index f23554369bc..ded50777ad2 100644 --- a/integrationExamples/gpt/prebidServer_example.html +++ b/integrationExamples/gpt/prebidServer_example.html @@ -33,31 +33,41 @@ pbjs.que.push(function() { var adUnits = [{ - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [[300, 250]] - } - }, - bids: [ - { - bidder: 'appnexus', - params: { - placementId: 13144370 - } + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [600, 500] + } + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 12883451 } - ] - }]; + } + ] + }]; + pbjs.bidderSettings = { + appnexus: { + bidCpmAdjustment: function () { + return 10; + } + } + } pbjs.setConfig({ bidderTimeout: 3000, s2sConfig : { accountId : '1', enabled : true, //default value set to false - defaultVendor: 'appnexus', + defaultVendor: 'appnexuspsp', bidders : ['appnexus'], timeout : 1000, //default value is 1000 adapter : 'prebidServer', //if we have any other s2s adapter, default value is s2s + }, + ortb2: { + test: 1 } }); diff --git a/integrationExamples/gpt/prebidServer_fledge_example.html b/integrationExamples/gpt/prebidServer_fledge_example.html index 8523c0f2920..eb2fc438997 100644 --- a/integrationExamples/gpt/prebidServer_fledge_example.html +++ b/integrationExamples/gpt/prebidServer_fledge_example.html @@ -50,7 +50,7 @@ s2sConfig: [{ accountId : '1', enabled : true, - defaultVendor: 'appnexus', + defaultVendor: 'appnexuspsp', bidders : ['openx'], timeout : 1500, adapter : 'prebidServer' diff --git a/integrationExamples/gpt/prebidServer_native_example.html b/integrationExamples/gpt/prebidServer_native_example.html index c590f0bcee5..a5fb0ffa894 100644 --- a/integrationExamples/gpt/prebidServer_native_example.html +++ b/integrationExamples/gpt/prebidServer_native_example.html @@ -114,7 +114,7 @@ s2sConfig: { accountId: '1', enabled: true, //default value set to false - bidders: ['appnexus'], + bidders: ['appnexuspsp'], timeout: 1000, //default value is 1000 adapter: 'prebidServer', //if we have any other s2s adapter, default value is s2s endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' diff --git a/integrationExamples/gpt/tpmn_example.html b/integrationExamples/gpt/tpmn_example.html new file mode 100644 index 00000000000..f215181c7e0 --- /dev/null +++ b/integrationExamples/gpt/tpmn_example.html @@ -0,0 +1,168 @@ + + + + + Prebid.js Banner Example + + + + + + + + + + +

Prebid.js TPMN Banner Example

+ +
+

Prebid.js TPMN Video Example

+
+ +
+
+
+ diff --git a/integrationExamples/gpt/tpmn_serverless_example.html b/integrationExamples/gpt/tpmn_serverless_example.html new file mode 100644 index 00000000000..0acaefbeb9c --- /dev/null +++ b/integrationExamples/gpt/tpmn_serverless_example.html @@ -0,0 +1,121 @@ + + + + + + + + + + + + + +

Ad Serverless Test Page

+ + +
+
+ + diff --git a/integrationExamples/noadserver/native_noadserver.html b/integrationExamples/noadserver/native_noadserver.html new file mode 100755 index 00000000000..81c71d2acfd --- /dev/null +++ b/integrationExamples/noadserver/native_noadserver.html @@ -0,0 +1,173 @@ + + + + + + + + + + + + + +

Prebid Native

+
+
+ +
+
+ + + + diff --git a/integrationExamples/topics/topics-server.js b/integrationExamples/topics/topics-server.js new file mode 100644 index 00000000000..0d248e5557c --- /dev/null +++ b/integrationExamples/topics/topics-server.js @@ -0,0 +1,72 @@ +// This is an example of a server-side endpoint that is utilizing the Topics API header functionality. +// Note: This test endpoint requires the following to run: node.js, npm, express, cors, body-parser + +const bodyParser = require('body-parser'); +const cors = require('cors'); +const express = require('express'); + +const port = process.env.PORT || 3000; + +const app = express(); +app.use(cors()); +app.use( + bodyParser.urlencoded({ + extended: true, + }) +); +app.use(bodyParser.json()); +app.use(express.static('public')); +app.set('port', port); + +const listener = app.listen(port, () => { + const host = + listener.address().address === '::' + ? 'http://localhost' + : 'http://' + listener.address().address; + // eslint-disable-next-line no-console + console.log( + `${__filename} is listening on ${host}:${listener.address().port}\n` + ); +}); + +app.get('*', (req, res) => { + res.setHeader('Observe-Browsing-Topics', '?1'); + + const resData = { + segment: { + domain: req.hostname, + topics: generateTopicArrayFromHeader(req.headers['sec-browsing-topics']), + bidder: req.query['bidder'], + }, + date: Date.now(), + }; + + res.json(resData); +}); + +const generateTopicArrayFromHeader = (topicString) => { + const result = []; + const topicArray = topicString.split(', '); + if (topicArray.length > 1) { + topicArray.pop(); + topicArray.map((topic) => { + const topicId = topic.split(';')[0]; + const versionsString = topic.split(';')[1].split('=')[1]; + const [config, taxonomy, model] = versionsString.split(':'); + const numTopicsWithSameVersions = topicId + .substring(1, topicId.length - 1) + .split(' '); + + numTopicsWithSameVersions.map((tpId) => { + result.push({ + topic: tpId, + version: versionsString, + configVersion: config, + taxonomyVersion: taxonomy, + modelVersion: model, + }); + }); + }); + } + return result; +}; diff --git a/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html b/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html index 044b4295e67..80ea81d09b6 100644 --- a/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html +++ b/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html @@ -37,7 +37,7 @@ divId: 'player', vendorCode: 1, // jwplayer vendorCode playerConfig: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', @@ -84,7 +84,7 @@ console.log('An Ad Impression came from a Bid: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html b/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html index 620f891fa50..663765317b0 100644 --- a/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html +++ b/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html @@ -20,7 +20,7 @@ divId: 'player', vendorCode: 1, // JW Player vendorCode playerConfig: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', diff --git a/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html b/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html new file mode 100644 index 00000000000..66eaff26090 --- /dev/null +++ b/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html @@ -0,0 +1,137 @@ + + + + + + + JW Player with Bids Back Handler override + + + + + +

JW Player with Bids Back Handler override

+
Div-1: Player placeholder div
+
+ + + diff --git a/integrationExamples/videoModule/jwplayer/eventListeners.html b/integrationExamples/videoModule/jwplayer/eventListeners.html index 0b43ff44d6c..39acb086107 100644 --- a/integrationExamples/videoModule/jwplayer/eventListeners.html +++ b/integrationExamples/videoModule/jwplayer/eventListeners.html @@ -29,7 +29,7 @@ } }] }]; - + pbjs.que.push(function () { pbjs.setConfig({ video: { @@ -37,7 +37,7 @@ divId: 'player', vendorCode: 1, // vendorCode for jwplayer playerConfig: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', @@ -237,7 +237,7 @@ console.log('The ad error resulted from a bid! \n', event); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/jwplayer/eventsUI.html b/integrationExamples/videoModule/jwplayer/eventsUI.html index 69ace5f6c2b..78d72a6153d 100644 --- a/integrationExamples/videoModule/jwplayer/eventsUI.html +++ b/integrationExamples/videoModule/jwplayer/eventsUI.html @@ -39,7 +39,7 @@ divId: 'player', vendorCode: 1, // jwplayer vendorCode playerConfig: { - licenseKey: '577+5vXsluqV2Uy0drAS8wrgiqJlYijZxz3DmoYDm8FTJjdoIe8zYA==', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { playlist: [{ @@ -278,7 +278,7 @@ eventHandler(e, "bidImpression", "auction", ); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html b/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html index 296a3265bd1..018c8eba8b2 100644 --- a/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html +++ b/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html @@ -36,7 +36,7 @@ divId: 'player', vendorCode: 1, // JW Player vendorCode playerConfig: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', @@ -106,7 +106,7 @@ console.log('An Ad Impression came from a Bid: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/jwplayer/mediaMetadata.html b/integrationExamples/videoModule/jwplayer/mediaMetadata.html index 815f5c4d6d7..7581af571d3 100644 --- a/integrationExamples/videoModule/jwplayer/mediaMetadata.html +++ b/integrationExamples/videoModule/jwplayer/mediaMetadata.html @@ -37,7 +37,7 @@ divId: 'player', vendorCode: 1, // JW Player vendorCode playerConfig: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { mediaid: 'XYXYXYXY', @@ -66,7 +66,7 @@ console.log('videos pb contentLoaded: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/jwplayer/playlist.html b/integrationExamples/videoModule/jwplayer/playlist.html index b77a1ec05fc..89efaea3d5c 100644 --- a/integrationExamples/videoModule/jwplayer/playlist.html +++ b/integrationExamples/videoModule/jwplayer/playlist.html @@ -38,7 +38,7 @@ vendorCode: 1, // JW Player vendorCode playerConfig: { params: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', vendorConfig: { playlist: [{ mediaid: 'XYXYXYXY', @@ -107,7 +107,7 @@ console.log('videos pb playlistComplete: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html b/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html index b645a58a4fc..d6656bc0c93 100644 --- a/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html +++ b/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html @@ -109,7 +109,7 @@ console.log('An Ad Impression came from a Bid: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/videojs/bidRequestScheduling.html b/integrationExamples/videoModule/videojs/bidRequestScheduling.html index 48f8b8685e1..eb10fda89a2 100644 --- a/integrationExamples/videoModule/videojs/bidRequestScheduling.html +++ b/integrationExamples/videoModule/videojs/bidRequestScheduling.html @@ -111,7 +111,7 @@ pbjs.onEvent('videoTime', (e) => { // when the video reaches the midroll, request bids and update the offset. if (e.position >= midrollOffset) { - pbjs.requestBids(adUnits); + pbjs.requestBids(); midrollOffset += 10; } }); diff --git a/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html b/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html new file mode 100644 index 00000000000..ac8f4163e76 --- /dev/null +++ b/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + VideoJS with Bids Back Handler override + + + + + +

VideoJS with Bids Back Handler override

+
Div-1: Player placeholder div
+ + + + + + diff --git a/integrationExamples/videoModule/videojs/eventListeners.html b/integrationExamples/videoModule/videojs/eventListeners.html index 16edaf4da41..1966f134e02 100644 --- a/integrationExamples/videoModule/videojs/eventListeners.html +++ b/integrationExamples/videoModule/videojs/eventListeners.html @@ -225,7 +225,7 @@ console.log('videos pb bid Error: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/videojs/eventsUI.html b/integrationExamples/videoModule/videojs/eventsUI.html index 04e0ca9eaf8..9eba09f7a52 100644 --- a/integrationExamples/videoModule/videojs/eventsUI.html +++ b/integrationExamples/videoModule/videojs/eventsUI.html @@ -301,7 +301,7 @@ eventHandler(e, "bidImpression", "auction", ); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/videojs/gamAdServerMediation.html b/integrationExamples/videoModule/videojs/gamAdServerMediation.html index 9efd77e9681..6ffc1a67c03 100644 --- a/integrationExamples/videoModule/videojs/gamAdServerMediation.html +++ b/integrationExamples/videoModule/videojs/gamAdServerMediation.html @@ -123,7 +123,7 @@ console.log('An Ad Impression came from a Bid: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/videojs/mediaMetadata.html b/integrationExamples/videoModule/videojs/mediaMetadata.html index eef12b0e19d..ede076fd814 100644 --- a/integrationExamples/videoModule/videojs/mediaMetadata.html +++ b/integrationExamples/videoModule/videojs/mediaMetadata.html @@ -87,7 +87,7 @@ console.log('videos pb contentLoaded: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/videojs/playlist.html b/integrationExamples/videoModule/videojs/playlist.html index a9e3d1bc99b..eb813f095f7 100644 --- a/integrationExamples/videoModule/videojs/playlist.html +++ b/integrationExamples/videoModule/videojs/playlist.html @@ -134,7 +134,7 @@ console.log('videos pb complete: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/libraries/appnexusKeywords/anKeywords.js b/libraries/appnexusUtils/anKeywords.js similarity index 91% rename from libraries/appnexusKeywords/anKeywords.js rename to libraries/appnexusUtils/anKeywords.js index 5dc0b453253..d6714dacc21 100644 --- a/libraries/appnexusKeywords/anKeywords.js +++ b/libraries/appnexusUtils/anKeywords.js @@ -1,4 +1,4 @@ -import {_each, deepAccess, getValueString, isArray, isStr, mergeDeep, isNumber} from '../../src/utils.js'; +import {_each, deepAccess, isArray, isNumber, isStr, mergeDeep, logWarn} from '../../src/utils.js'; import {getAllOrtbKeywords} from '../keywords/keywords.js'; import {CLIENT_SECTIONS} from '../../src/fpd/oneClient.js'; @@ -12,6 +12,19 @@ const ORTB_SEG_PATHS = ['user.data'].concat( CLIENT_SECTIONS.map((prefix) => `${prefix}.content.data`) ); +function getValueString(param, val, defaultValue) { + if (val === undefined || val === null) { + return defaultValue; + } + if (isStr(val)) { + return val; + } + if (isNumber(val)) { + return val.toString(); + } + logWarn('Unsuported type for param: ' + param + ' required type: String'); +} + /** * Converts an object of arrays (either strings or numbers) into an array of objects containing key and value properties * normally read from bidder params diff --git a/libraries/appnexusUtils/anUtils.js b/libraries/appnexusUtils/anUtils.js new file mode 100644 index 00000000000..9b55cd5c2a4 --- /dev/null +++ b/libraries/appnexusUtils/anUtils.js @@ -0,0 +1,25 @@ +/** + * Converts a string value in camel-case to underscore eg 'placementId' becomes 'placement_id' + * @param {string} value string value to convert + */ +import {deepClone, isPlainObject} from '../../src/utils.js'; + +export function convertCamelToUnderscore(value) { + return value.replace(/(?:^|\.?)([A-Z])/g, function (x, y) { + return '_' + y.toLowerCase(); + }).replace(/^_/, ''); +} + +/** + * Creates an array of n length and fills each item with the given value + */ +export function fill(value, length) { + let newArray = []; + + for (let i = 0; i < length; i++) { + let valueToPush = isPlainObject(value) ? deepClone(value) : value; + newArray.push(valueToPush); + } + + return newArray; +} diff --git a/libraries/chunk/chunk.js b/libraries/chunk/chunk.js new file mode 100644 index 00000000000..57be7bd5016 --- /dev/null +++ b/libraries/chunk/chunk.js @@ -0,0 +1,19 @@ +/** + * http://npm.im/chunk + * Returns an array with *size* chunks from given array + * + * Example: + * ['a', 'b', 'c', 'd', 'e'] chunked by 2 => + * [['a', 'b'], ['c', 'd'], ['e']] + */ +export function chunk(array, size) { + let newArray = []; + + for (let i = 0; i < Math.ceil(array.length / size); i++) { + let start = i * size; + let end = start + size; + newArray.push(array.slice(start, end)); + } + + return newArray; +} diff --git a/libraries/cmp/cmpClient.js b/libraries/cmp/cmpClient.js new file mode 100644 index 00000000000..03a50c37bb3 --- /dev/null +++ b/libraries/cmp/cmpClient.js @@ -0,0 +1,169 @@ +import {GreedyPromise} from '../../src/utils/promise.js'; + +/** + * @typedef {function} CMPClient + * + * @param {{}} params CMP parameters. Currently this is a subset of {command, callback, parameter, version}. + * @param {bool} once if true, discard cross-frame event listeners once a reply message is received. + * @returns {Promise<*>} a promise to the API's "result" - see the `mode` argument to `cmpClient` on how that's determined. + * @property {boolean} isDirect true if the CMP is directly accessible (no postMessage required) + * @property {() => void} close close the client; currently, this just stops listening for cross-frame messages. + */ + +/** + * Returns a client function that can interface with a CMP regardless of where it's located. + * + * @param apiName name of the CMP api, e.g. "__gpp" + * @param apiVersion? CMP API version + * @param apiArgs? names of the arguments taken by the api function, in order. + * @param callbackArgs? names of the cross-frame response payload properties that should be passed as callback arguments, in order + * @param mode? controls the callbacks passed to the underlying API, and how the promises returned by the client are resolved. + * + * The client behaves differently when it's provided a `callback` argument vs when it's not - for short, let's name these + * cases "subscriptions" and "one-shot calls" respectively: + * + * With `mode: MODE_MIXED` (the default), promises returned on subscriptions are resolved to undefined when the callback + * is first run (that is, the promise resolves when the CMP replies, but what it replies with is discarded and + * left for the callback to deal with). For one-shot calls, the returned promise is resolved to the API's + * return value when it's directly accessible, or with the result from the first (and, presumably, the only) + * cross-frame reply when it's not; + * + * With `mode: MODE_RETURN`, the returned promise always resolves to the API's return value - which is taken to be undefined + * when cross-frame; + * + * With `mode: MODE_CALLBACK`, the underlying API is expected to never directly return anything significant; instead, + * it should always accept a callback and - for one-shot calls - invoke it only once with the result. The client will + * automatically generate an appropriate callback for one-shot calls and use the result it's given to resolve + * the returned promise. Subscriptions are treated in the same way as MODE_MIXED. + * + * @param win + * @returns {CMPClient} CMP invocation function (or null if no CMP was found). + */ + +export const MODE_MIXED = 0; +export const MODE_RETURN = 1; +export const MODE_CALLBACK = 2; + +export function cmpClient( + { + apiName, + apiVersion, + apiArgs = ['command', 'callback', 'parameter', 'version'], + callbackArgs = ['returnValue', 'success'], + mode = MODE_MIXED, + }, + win = window +) { + const cmpCallbacks = {}; + const callName = `${apiName}Call`; + const cmpDataPkgName = `${apiName}Return`; + + function handleMessage(event) { + const json = (typeof event.data === 'string' && event.data.includes(cmpDataPkgName)) ? JSON.parse(event.data) : event.data; + if (json?.[cmpDataPkgName]?.callId) { + const payload = json[cmpDataPkgName]; + + if (cmpCallbacks.hasOwnProperty(payload.callId)) { + cmpCallbacks[payload.callId](...callbackArgs.map(name => payload[name])); + } + } + } + + function findCMP() { + let f = win; + let cmpFrame; + let isDirect = false; + while (f != null) { + try { + if (typeof f[apiName] === 'function') { + cmpFrame = f; + isDirect = true; + break; + } + } catch (e) { + } + + // need separate try/catch blocks due to the exception errors thrown when trying to check for a frame that doesn't exist in 3rd party env + try { + if (f.frames[`${apiName}Locator`]) { + cmpFrame = f; + break; + } + } catch (e) { + } + + if (f === win.top) break; + f = f.parent; + } + + return [ + cmpFrame, + isDirect + ]; + } + + const [cmpFrame, isDirect] = findCMP(); + + if (!cmpFrame) { + return; + } + + function resolveParams(params) { + params = Object.assign({version: apiVersion}, params); + return apiArgs.map(arg => [arg, params[arg]]) + } + + function wrapCallback(callback, resolve, reject, preamble) { + const haveCb = typeof callback === 'function'; + + return function (result, success) { + preamble && preamble(); + if (mode !== MODE_RETURN) { + const resolver = success == null || success ? resolve : reject; + resolver(haveCb ? undefined : result); + } + haveCb && callback.apply(this, arguments); + } + } + + let client; + + if (isDirect) { + client = function invokeCMPDirect(params = {}) { + return new GreedyPromise((resolve, reject) => { + const ret = cmpFrame[apiName](...resolveParams({ + ...params, + callback: (params.callback || mode === MODE_CALLBACK) ? wrapCallback(params.callback, resolve, reject) : undefined, + }).map(([_, val]) => val)); + if (mode === MODE_RETURN || (params.callback == null && mode === MODE_MIXED)) { + resolve(ret); + } + }); + }; + } else { + win.addEventListener('message', handleMessage, false); + + client = function invokeCMPFrame(params, once = false) { + return new GreedyPromise((resolve, reject) => { + // call CMP via postMessage + const callId = Math.random().toString(); + const msg = { + [callName]: { + ...Object.fromEntries(resolveParams(params).filter(([param]) => param !== 'callback')), + callId: callId + } + }; + + cmpCallbacks[callId] = wrapCallback(params?.callback, resolve, reject, (once || params?.callback == null) && (() => { delete cmpCallbacks[callId] })); + cmpFrame.postMessage(msg, '*'); + if (mode === MODE_RETURN) resolve(); + }); + }; + } + return Object.assign(client, { + isDirect, + close() { + !isDirect && win.removeEventListener('message', handleMessage); + } + }) +} diff --git a/libraries/currencyUtils/currency.js b/libraries/currencyUtils/currency.js new file mode 100644 index 00000000000..924f8f200d8 --- /dev/null +++ b/libraries/currencyUtils/currency.js @@ -0,0 +1,31 @@ +import {getGlobal} from '../../src/prebidGlobal.js'; +import {keyCompare} from '../../src/utils/reducers.js'; + +/** + * Attempt to convert `amount` from the currency `fromCur` to the currency `toCur`. + * + * By default, when the conversion is not possible (currency module not present or + * throwing errors), the amount is returned unchanged. This behavior can be + * toggled off with bestEffort = false. + */ +export function convertCurrency(amount, fromCur, toCur, bestEffort = true) { + if (fromCur === toCur) return amount; + let result = amount; + try { + result = getGlobal().convertCurrency(amount, fromCur, toCur); + } catch (e) { + if (!bestEffort) throw e; + } + return result; +} + +export function currencyNormalizer(toCurrency = null, bestEffort = true, convert = convertCurrency) { + return function (amount, currency) { + if (toCurrency == null) toCurrency = currency; + return convert(amount, currency, toCurrency, bestEffort); + } +} + +export function currencyCompare(get = (obj) => [obj.cpm, obj.currency], normalize = currencyNormalizer()) { + return keyCompare(obj => normalize.apply(null, get(obj))) +} diff --git a/libraries/gptUtils/gptUtils.js b/libraries/gptUtils/gptUtils.js new file mode 100644 index 00000000000..950f28c618f --- /dev/null +++ b/libraries/gptUtils/gptUtils.js @@ -0,0 +1,37 @@ +import {find} from '../../src/polyfill.js'; +import {compareCodeAndSlot, isGptPubadsDefined} from '../../src/utils.js'; + +/** + * Returns filter function to match adUnitCode in slot + * @param {string} adUnitCode AdUnit code + * @return {function} filter function + */ +export function isSlotMatchingAdUnitCode(adUnitCode) { + return (slot) => compareCodeAndSlot(slot, adUnitCode); +} + +/** + * @summary Uses the adUnit's code in order to find a matching gpt slot object on the page + */ +export function getGptSlotForAdUnitCode(adUnitCode) { + let matchingSlot; + if (isGptPubadsDefined()) { + // find the first matching gpt slot on the page + matchingSlot = find(window.googletag.pubads().getSlots(), isSlotMatchingAdUnitCode(adUnitCode)); + } + return matchingSlot; +} + +/** + * @summary Uses the adUnit's code in order to find a matching gptSlot on the page + */ +export function getGptSlotInfoForAdUnitCode(adUnitCode) { + const matchingSlot = getGptSlotForAdUnitCode(adUnitCode); + if (matchingSlot) { + return { + gptSlot: matchingSlot.getAdUnitPath(), + divId: matchingSlot.getSlotElementId() + }; + } + return {}; +} diff --git a/libraries/htmlEscape/htmlEscape.js b/libraries/htmlEscape/htmlEscape.js new file mode 100644 index 00000000000..f0952c02e3c --- /dev/null +++ b/libraries/htmlEscape/htmlEscape.js @@ -0,0 +1,26 @@ +/** + * Encode a string for inclusion in HTML. + * See https://pragmaticwebsecurity.com/articles/spasecurity/json-stringify-xss.html and + * https://codeql.github.com/codeql-query-help/javascript/js-bad-code-sanitization/ + * @return {string} + */ +export const escapeUnsafeChars = (() => { + const escapes = { + '<': '\\u003C', + '>': '\\u003E', + '/': '\\u002F', + '\\': '\\\\', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + '\0': '\\0', + '\u2028': '\\u2028', + '\u2029': '\\u2029' + }; + + return function (str) { + return str.replace(/[<>\b\f\n\r\t\0\u2028\u2029\\]/g, x => escapes[x]); + }; +})(); diff --git a/libraries/mspa/activityControls.js b/libraries/mspa/activityControls.js new file mode 100644 index 00000000000..eaf515e2385 --- /dev/null +++ b/libraries/mspa/activityControls.js @@ -0,0 +1,127 @@ +import {registerActivityControl} from '../../src/activities/rules.js'; +import { + ACTIVITY_ENRICH_EIDS, + ACTIVITY_ENRICH_UFPD, + ACTIVITY_SYNC_USER, + ACTIVITY_TRANSMIT_PRECISE_GEO +} from '../../src/activities/activities.js'; +import {gppDataHandler} from '../../src/adapterManager.js'; +import {logInfo} from '../../src/utils.js'; + +// default interpretation for MSPA consent(s): +// https://docs.prebid.org/features/mspa-usnat.html + +const SENSITIVE_DATA_GEO = 7; + +function isApplicable(val) { + return val != null && val !== 0 +} + +export function isBasicConsentDenied(cd) { + // service provider mode is always consent denied + return ['MspaServiceProviderMode', 'Gpc'].some(prop => cd[prop] === 1) || + // you cannot consent to what you were not notified of + cd.PersonalDataConsents === 2 || + // minors 13+ who have not given consent + cd.KnownChildSensitiveDataConsents[0] === 1 || + // minors under 13 cannot consent + isApplicable(cd.KnownChildSensitiveDataConsents[1]) || + // covered cannot be zero + cd.MspaCoveredTransaction === 0; +} + +export function sensitiveNoticeIs(cd, value) { + return ['SensitiveDataProcessingOptOutNotice', 'SensitiveDataLimitUseNotice'].some(prop => cd[prop] === value) +} + +export function isConsentDenied(cd) { + return isBasicConsentDenied(cd) || + ['Sale', 'Sharing', 'TargetedAdvertising'].some(scope => { + const oo = cd[`${scope}OptOut`]; + const notice = cd[`${scope}OptOutNotice`]; + // user opted out + return oo === 1 || + // opt-out notice was not given + notice === 2 || + // do not trust CMP if it signals opt-in but no opt-out notice was given + (oo === 2 && notice === 0); + }) || + // no sharing notice was given ... + cd.SharingNotice === 2 || + // ... or the CMP says it was not applicable, while also claiming it got consent + (cd.SharingOptOut === 2 && cd.SharingNotice === 0); +} + +export const isTransmitUfpdConsentDenied = (() => { + // deny anything that smells like: genetic, biometric, state/national ID, financial, union membership, + // or personal communication data + const cannotBeInScope = [6, 7, 9, 10, 12].map(el => --el); + // require consent for everything else (except geo, which is treated separately) + const allExceptGeo = Array.from(Array(12).keys()).filter((el) => el !== SENSITIVE_DATA_GEO) + const mustHaveConsent = allExceptGeo.filter(el => !cannotBeInScope.includes(el)); + + return function (cd) { + return isConsentDenied(cd) || + // no notice about sensitive data was given + sensitiveNoticeIs(cd, 2) || + // extra-sensitive data is applicable + cannotBeInScope.some(i => isApplicable(cd.SensitiveDataProcessing[i])) || + // user opted out for not-as-sensitive data + mustHaveConsent.some(i => cd.SensitiveDataProcessing[i] === 1) || + // CMP says it has consent, but did not give notice about it + (sensitiveNoticeIs(cd, 0) && allExceptGeo.some(i => cd.SensitiveDataProcessing[i] === 2)) + } +})(); + +export function isTransmitGeoConsentDenied(cd) { + const geoConsent = cd.SensitiveDataProcessing[SENSITIVE_DATA_GEO]; + return geoConsent === 1 || + isBasicConsentDenied(cd) || + // no sensitive data notice was given + sensitiveNoticeIs(cd, 2) || + // do not trust CMP if it says it has consent for geo but didn't show a sensitive data notice + (sensitiveNoticeIs(cd, 0) && geoConsent === 2) +} + +const CONSENT_RULES = { + [ACTIVITY_SYNC_USER]: isConsentDenied, + [ACTIVITY_ENRICH_EIDS]: isConsentDenied, + [ACTIVITY_ENRICH_UFPD]: isTransmitUfpdConsentDenied, + [ACTIVITY_TRANSMIT_PRECISE_GEO]: isTransmitGeoConsentDenied +}; + +export function mspaRule(sids, getConsent, denies, applicableSids = () => gppDataHandler.getConsentData()?.applicableSections) { + return function () { + if (applicableSids().some(sid => sids.includes(sid))) { + const consent = getConsent(); + if (consent == null) { + return {allow: false, reason: 'consent data not available'}; + } + if (denies(consent)) { + return {allow: false}; + } + } + }; +} + +function flatSection(subsections) { + if (subsections == null) return subsections; + return subsections.reduceRight((subsection, consent) => { + return Object.assign(consent, subsection); + }, {}); +} + +export function setupRules(api, sids, normalizeConsent = (c) => c, rules = CONSENT_RULES, registerRule = registerActivityControl, getConsentData = () => gppDataHandler.getConsentData()) { + const unreg = []; + const ruleName = `MSPA (GPP '${api}' for section${sids.length > 1 ? 's' : ''} ${sids.join(', ')})`; + logInfo(`Enabling activity controls for ${ruleName}`) + Object.entries(rules).forEach(([activity, denies]) => { + unreg.push(registerRule(activity, ruleName, mspaRule( + sids, + () => normalizeConsent(flatSection(getConsentData()?.parsedSections?.[api])), + denies, + () => getConsentData()?.applicableSections || [] + ))); + }); + return () => unreg.forEach(ur => ur()); +} diff --git a/libraries/sizeUtils/sizeUtils.js b/libraries/sizeUtils/sizeUtils.js new file mode 100644 index 00000000000..41cdd71df89 --- /dev/null +++ b/libraries/sizeUtils/sizeUtils.js @@ -0,0 +1,29 @@ +/** + * Read an adUnit object and return the sizes used in an [[728, 90]] format (even if they had [728, 90] defined) + * Preference is given to the `adUnit.mediaTypes.banner.sizes` object over the `adUnit.sizes` + * @param {object} adUnit one adUnit object from the normal list of adUnits + * @returns {Array.} array of arrays containing numeric sizes + */ +export function getAdUnitSizes(adUnit) { + if (!adUnit) { + return; + } + + let sizes = []; + if (adUnit.mediaTypes && adUnit.mediaTypes.banner && Array.isArray(adUnit.mediaTypes.banner.sizes)) { + let bannerSizes = adUnit.mediaTypes.banner.sizes; + if (Array.isArray(bannerSizes[0])) { + sizes = bannerSizes; + } else { + sizes.push(bannerSizes); + } + // TODO - remove this else block when we're ready to deprecate adUnit.sizes for bidders + } else if (Array.isArray(adUnit.sizes)) { + if (Array.isArray(adUnit.sizes[0])) { + sizes = adUnit.sizes; + } else { + sizes.push(adUnit.sizes); + } + } + return sizes; +} diff --git a/libraries/transformParamsUtils/convertTypes.js b/libraries/transformParamsUtils/convertTypes.js new file mode 100644 index 00000000000..813d8e6e693 --- /dev/null +++ b/libraries/transformParamsUtils/convertTypes.js @@ -0,0 +1,36 @@ +import {isFn} from '../../src/utils.js'; + +/** + * Try to convert a value to a type. + * If it can't be done, the value will be returned. + * + * @param {string} typeToConvert The target type. e.g. "string", "number", etc. + * @param {*} value The value to be converted into typeToConvert. + */ +function tryConvertType(typeToConvert, value) { + if (typeToConvert === 'string') { + return value && value.toString(); + } else if (typeToConvert === 'number') { + return Number(value); + } else { + return value; + } +} + +export function convertTypes(types, params) { + Object.keys(types).forEach(key => { + if (params[key]) { + if (isFn(types[key])) { + params[key] = types[key](params[key]); + } else { + params[key] = tryConvertType(types[key], params[key]); + } + + // don't send invalid values + if (isNaN(params[key])) { + delete params.key; + } + } + }); + return params; +} diff --git a/libraries/uid2Eids/uid2Eids.js b/libraries/uid2Eids/uid2Eids.js new file mode 100644 index 00000000000..ce4f4fa3b2a --- /dev/null +++ b/libraries/uid2Eids/uid2Eids.js @@ -0,0 +1,14 @@ +export const UID2_EIDS = { + 'uid2': { + source: 'uidapi.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + } +} diff --git a/libraries/urlUtils/urlUtils.js b/libraries/urlUtils/urlUtils.js new file mode 100644 index 00000000000..f0c5823aab1 --- /dev/null +++ b/libraries/urlUtils/urlUtils.js @@ -0,0 +1,7 @@ +export function tryAppendQueryString(existingUrl, key, value) { + if (value) { + return existingUrl + key + '=' + encodeURIComponent(value) + '&'; + } + + return existingUrl; +} diff --git a/modules/.submodules.json b/modules/.submodules.json index b45fb7f2303..830d3c9b2f3 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -46,7 +46,8 @@ "zeotapIdPlusIdSystem", "adqueryIdSystem", "gravitoIdSystem", - "freepassIdSystem" + "freepassIdSystem", + "operaadsIdSystem" ], "adpod": [ "freeWheelAdserverVideo", @@ -54,30 +55,44 @@ ], "rtdModule": [ "1plusXRtdProvider", + "a1MediaRtdProvider", "aaxBlockmeterRtdProvider", + "adlooxRtdProvider", + "adnuntiusRtdProvider", "airgridRtdProvider", "akamaiDapRtdProvider", "arcspanRtdProvider", "blueconicRtdProvider", + "brandmetricsRtdProvider", "browsiRtdProvider", "captifyRtdProvider", + "mediafilterRtdProvider", "confiantRtdProvider", "dgkeywordRtdProvider", + "experianRtdProvider", "geoedgeRtdProvider", + "geolocationRtdProvider", + "greenbidsRtdProvider", + "growthCodeRtdProvider", "hadronRtdProvider", - "haloRtdProvider", "iasRtdProvider", + "idWardRtdProvider", + "imRtdProvider", + "intersectionRtdProvider", "jwplayerRtdProvider", "medianetRtdProvider", "mgidRtdProvider", + "neuwoRtdProvider", "oneKeyRtdProvider", "optimeraRtdProvider", + "oxxionRtdProvider", "permutiveRtdProvider", + "qortexRtdProvider", "reconciliationRtdProvider", + "relevadRtdProvider", "sirdataRtdProvider", "timeoutRtdProvider", - "weboramaRtdProvider", - "zeusPrimeRtdProvider" + "weboramaRtdProvider" ], "fpdModule": [ "validationFpdModule", diff --git a/modules/33acrossAnalyticsAdapter.js b/modules/33acrossAnalyticsAdapter.js new file mode 100644 index 00000000000..e3539906b13 --- /dev/null +++ b/modules/33acrossAnalyticsAdapter.js @@ -0,0 +1,656 @@ +import { deepAccess, logInfo, logWarn, logError, deepClone } from '../src/utils.js'; +import buildAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager, { coppaDataHandler, gdprDataHandler, gppDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; + +/** + * @typedef {typeof import('../src/constants.json').EVENTS} EVENTS + */ +const { EVENTS } = CONSTANTS; + +/** @typedef {'pending'|'available'|'targetingSet'|'rendered'|'timeout'|'rejected'|'noBid'|'error'} BidStatus */ +/** + * @type {Object} + */ +const BidStatus = { + PENDING: 'pending', + AVAILABLE: 'available', + TARGETING_SET: 'targetingSet', + RENDERED: 'rendered', + TIMEOUT: 'timeout', + REJECTED: 'rejected', + NOBID: 'noBid', + ERROR: 'error', +} + +const ANALYTICS_VERSION = '1.0.0'; +const PROVIDER_NAME = '33across'; +const GVLID = 58; +/** Time to wait for all transactions in an auction to complete before sending the report */ +const DEFAULT_TRANSACTION_TIMEOUT = 10000; +/** Time to wait after all GAM slots have registered before sending the report */ +export const POST_GAM_TIMEOUT = 500; +export const DEFAULT_ENDPOINT = 'https://analytics.33across.com/api/v1/event'; + +export const log = getLogger(); + +/** + * @typedef {Object} AnalyticsReport - Sent when all bids are complete (as determined by `bidWon` and `slotRenderEnded` events) + * @property {string} analyticsVersion - Version of the Prebid.js 33Across Analytics Adapter + * @property {string} pid - Partner ID + * @property {string} src - Source of the report ('pbjs') + * @property {string} pbjsVersion - Version of Prebid.js + * @property {Auction[]} auctions + */ + +/** + * @typedef {Object} AnalyticsCache + * @property {string} pid Partner ID + * @property {Object} auctions + * @property {string} [usPrivacy] + */ + +/** + * @typedef {Object} Auction - Parsed auction data + * @property {AdUnit[]} adUnits + * @property {string} auctionId + * @property {string[]} userIds + */ + +/** + * @typedef {`${number}x${number}`} AdUnitSize + */ + +/** + * @typedef {('banner'|'native'|'video')} AdUnitMediaType + */ + +/** + * @typedef {Object} BidResponse + * @property {number} cpm + * @property {string} cur + * @property {number} [cpmOrig] + * @property {number} cpmFloor + * @property {AdUnitMediaType} mediaType + * @property {AdUnitSize} size + */ + +/** + * @typedef {Object} Bid - Parsed bid data + * @property {string} bidder + * @property {string} bidId + * @property {string} source + * @property {string} status + * @property {BidResponse} [bidResponse] + * @property {1|0} [hasWon] + */ + +/** + * @typedef {Object} AdUnit - Parsed adUnit data + * @property {string} transactionId - Primary key for *this* auction/adUnit combination + * @property {string} adUnitCode + * @property {string} slotId - Equivalent to GPID. (Note that + * GPID supports adUnits where multiple units have the same `code` values + * by appending a `#UNIQUIFIER`. The value of the UNIQUIFIER is likely to be the div-id, + * but, if div-id is randomized / unavailable, may be something else like the media size) + * @property {Array} mediaTypes + * @property {Array} sizes + * @property {Array} bids + */ + +/** + * After the first transaction begins, wait until all transactions are complete + * before calling `onComplete`. If the timeout is reached before all transactions + * are complete, send the report anyway. + * + * Use this to track all transactions per auction, and send the report as soon + * as all adUnits have been won (or after timeout) even if other bid/auction + * activity is still happening. + */ +class TransactionManager { + /** + * Milliseconds between activity to allow until this collection automatically completes. + * @type {number} + */ + #sendTimeout; + #sendTimeoutId; + #transactionsPending = new Set(); + #transactionsCompleted = new Set(); + #onComplete; + + constructor({ timeout, onComplete }) { + this.#sendTimeout = timeout; + this.#onComplete = onComplete; + } + + status() { + return { + pending: [...this.#transactionsPending], + completed: [...this.#transactionsCompleted], + }; + } + + initiate(transactionId) { + this.#transactionsPending.add(transactionId); + this.#restartSendTimeout(); + } + + complete(transactionId) { + if (!this.#transactionsPending.has(transactionId)) { + log.warn(`transactionId "${transactionId}" was not found. No transaction to mark as complete.`); + return; + } + + this.#transactionsPending.delete(transactionId); + this.#transactionsCompleted.add(transactionId); + + if (this.#transactionsPending.size === 0) { + this.#flushTransactions(); + } + } + + #flushTransactions() { + this.#clearSendTimeout(); + this.#transactionsPending = new Set(); + this.#onComplete(); + } + + // gulp-eslint is using eslint 6, a version that doesn't support private method syntax + // eslint-disable-next-line no-dupe-class-members + #clearSendTimeout() { + return clearTimeout(this.#sendTimeoutId); + } + + // eslint-disable-next-line no-dupe-class-members + #restartSendTimeout() { + this.#clearSendTimeout(); + + this.#sendTimeoutId = setTimeout(() => { + if (this.#sendTimeout !== 0) { + log.warn(`Timed out waiting for ad transactions to complete. Sending report.`); + } + + this.#flushTransactions(); + }, this.#sendTimeout); + } +} + +/** + * Initialized during `enableAnalytics`. Exported for testing purposes. + */ +export const locals = { + /** @type {Object} - one manager per auction */ + transactionManagers: {}, + /** @type {AnalyticsCache} */ + cache: { + auctions: {}, + pid: '', + }, + /** @type {Object} */ + adUnitMap: {}, + reset() { + this.transactionManagers = {}; + this.cache = { + auctions: {}, + pid: '', + }; + this.adUnitMap = {}; + } +} + +/** + * @typedef {Object} AnalyticsAdapter + * @property {function} track + * @property {function} enableAnalytics + * @property {function} disableAnalytics + * @property {function} [originEnableAnalytics] + * @property {function} [originDisableAnalytics] + * @property {function} [_oldEnable] + */ + +/** + * @type {AnalyticsAdapter} + */ +const analyticsAdapter = Object.assign( + buildAdapter({ analyticsType: 'endpoint' }), + { track: analyticEventHandler } +); + +analyticsAdapter.originEnableAnalytics = analyticsAdapter.enableAnalytics; +analyticsAdapter.enableAnalytics = enableAnalyticsWrapper; + +/** + * @typedef {Object} AnalyticsConfig + * @property {string} provider - set by pbjs at module registration time + * @property {Object} options + * @property {string} options.pid - Publisher/Partner ID + * @property {string} [options.endpoint=DEFAULT_ENDPOINT] - Endpoint to send analytics data + * @property {number} [options.timeout=DEFAULT_TRANSACTION_TIMEOUT] - Timeout for sending analytics data + */ + +/** + * @param {AnalyticsConfig} config Analytics module configuration + */ +function enableAnalyticsWrapper(config) { + const { options } = config; + + const pid = options.pid; + if (!pid) { + log.error('No partnerId provided for "options.pid". No analytics will be sent.'); + + return; + } + + const endpoint = calculateEndpoint(options.endpoint); + this.getUrl = () => endpoint; + + const timeout = calculateTransactionTimeout(options.timeout); + this.getTimeout = () => timeout; + + locals.cache = { + pid, + auctions: {}, + }; + + window.googletag = window.googletag || { cmd: [] }; + window.googletag.cmd.push(subscribeToGamSlots); + + analyticsAdapter.originEnableAnalytics(config); +} + +/** + * @param {string} [endpoint] + * @returns {string} + */ +function calculateEndpoint(endpoint = DEFAULT_ENDPOINT) { + if (typeof endpoint === 'string' && endpoint.startsWith('http')) { + return endpoint; + } + + log.info(`Invalid endpoint provided for "options.endpoint". Using default endpoint.`); + + return DEFAULT_ENDPOINT; +} +/** + * @param {number} [configTimeout] + * @returns {number} Transaction Timeout + */ +function calculateTransactionTimeout(configTimeout = DEFAULT_TRANSACTION_TIMEOUT) { + if (typeof configTimeout === 'number' && configTimeout >= 0) { + return configTimeout; + } + + log.info(`Invalid timeout provided for "options.timeout". Using default timeout of ${DEFAULT_TRANSACTION_TIMEOUT}ms.`); + + return DEFAULT_TRANSACTION_TIMEOUT; +} + +function subscribeToGamSlots() { + window.googletag.pubads().addEventListener('slotRenderEnded', event => { + setTimeout(() => { + const { transactionId, auctionId } = + getAdUnitMetadata(event.slot.getAdUnitPath(), event.slot.getSlotElementId()); + if (!transactionId || !auctionId) { + const slotName = `${event.slot.getAdUnitPath()} - ${event.slot.getSlotElementId()}`; + log.warn('Could not find configured ad unit matching GAM render of slot:', { slotName }); + return; + } + + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].complete(transactionId); + }, POST_GAM_TIMEOUT); + }); +} + +function getAdUnitMetadata(adUnitPath, adSlotElementId) { + const adUnitMeta = locals.adUnitMap[adUnitPath] || locals.adUnitMap[adSlotElementId]; + if (adUnitMeta && adUnitMeta.length > 0) { + return adUnitMeta[adUnitMeta.length - 1]; + } + return {}; +} + +/** necessary for testing */ +analyticsAdapter.originDisableAnalytics = analyticsAdapter.disableAnalytics; +analyticsAdapter.disableAnalytics = function () { + analyticsAdapter._oldEnable = enableAnalyticsWrapper; + locals.reset(); + analyticsAdapter.originDisableAnalytics(); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: analyticsAdapter, + code: PROVIDER_NAME, + gvlid: GVLID, +}); + +export default analyticsAdapter; + +/** + * @param {AnalyticsCache} analyticsCache + * @param {string} completedAuctionId value of auctionId + * @return {AnalyticsReport} Analytics report + */ +function createReportFromCache(analyticsCache, completedAuctionId) { + const { pid, auctions } = analyticsCache; + + const report = { + pid, + src: 'pbjs', + analyticsVersion: ANALYTICS_VERSION, + pbjsVersion: '$prebid.version$', // Replaced by build script + auctions: [ auctions[completedAuctionId] ], + } + if (uspDataHandler.getConsentData()) { + report.usPrivacy = uspDataHandler.getConsentData(); + } + + if (gdprDataHandler.getConsentData()) { + report.gdpr = Number(Boolean(gdprDataHandler.getConsentData().gdprApplies)); + report.gdprConsent = gdprDataHandler.getConsentData().consentString || ''; + } + + if (gppDataHandler.getConsentData()) { + report.gpp = gppDataHandler.getConsentData().gppString; + report.gppSid = gppDataHandler.getConsentData().applicableSections; + } + + if (coppaDataHandler.getCoppa()) { + report.coppa = Number(coppaDataHandler.getCoppa()); + } + + return report; +} + +function getCachedBid(auctionId, bidId) { + const auction = locals.cache.auctions[auctionId]; + for (let adUnit of auction.adUnits) { + for (let bid of adUnit.bids) { + if (bid.bidId === bidId) { + return bid; + } + } + } + log.error(`Cannot find bid "${bidId}" in auction "${auctionId}".`); +}; + +/** + * @param {Object} args + * @param {Object} args.args Event data + * @param {EVENTS[keyof EVENTS]} args.eventType + */ +function analyticEventHandler({ eventType, args }) { + if (!locals.cache) { + log.error('Something went wrong. Analytics cache is not initialized.'); + return; + } + + switch (eventType) { + case EVENTS.AUCTION_INIT: + onAuctionInit(args); + break; + case EVENTS.BID_REQUESTED: // BidStatus.PENDING + onBidRequested(args); + break; + case EVENTS.BID_TIMEOUT: + for (let bid of args) { + setCachedBidStatus(bid.auctionId, bid.bidId, BidStatus.TIMEOUT); + } + break; + case EVENTS.BID_RESPONSE: + onBidResponse(args); + break; + case EVENTS.BID_REJECTED: + onBidRejected(args); + break; + case EVENTS.NO_BID: + case EVENTS.SEAT_NON_BID: + setCachedBidStatus(args.auctionId, args.bidId, BidStatus.NOBID); + break; + case EVENTS.BIDDER_ERROR: + if (args.bidderRequest && args.bidderRequest.bids) { + for (let bid of args.bidderRequest.bids) { + setCachedBidStatus(args.bidderRequest.auctionId, bid.bidId, BidStatus.ERROR); + } + } + break; + case EVENTS.AUCTION_END: + onAuctionEnd(args); + break; + case EVENTS.BID_WON: // BidStatus.TARGETING_SET | BidStatus.RENDERED | BidStatus.ERROR + onBidWon(args); + break; + default: + break; + } +} + +/**************** + * AUCTION_INIT * + ***************/ +function onAuctionInit({ adUnits, auctionId, bidderRequests }) { + if (typeof auctionId !== 'string' || !Array.isArray(bidderRequests)) { + log.error('Analytics adapter failed to parse auction.'); + return; + } + + locals.cache.auctions[auctionId] = { + auctionId, + adUnits: adUnits.map(au => { + setAdUnitMap(au.code, auctionId, au.transactionId); + + return { + transactionId: au.transactionId, + adUnitCode: au.code, + // Note: GPID supports adUnits that have matching `code` values by appending a `#UNIQUIFIER`. + // The value of the UNIQUIFIER is likely to be the div-id, + // but, if div-id is randomized / unavailable, may be something else like the media size) + slotId: deepAccess(au, 'ortb2Imp.ext.gpid') || deepAccess(au, 'ortb2Imp.ext.data.pbadslot', au.code), + mediaTypes: Object.keys(au.mediaTypes), + sizes: au.sizes.map(size => size.join('x')), + bids: [], + } + }), + userIds: Object.keys(deepAccess(bidderRequests, '0.bids.0.userId', {})), + }; + + locals.transactionManagers[auctionId] ||= + new TransactionManager({ + timeout: analyticsAdapter.getTimeout(), + onComplete() { + sendReport( + createReportFromCache(locals.cache, auctionId), + analyticsAdapter.getUrl() + ); + delete locals.transactionManagers[auctionId]; + } + }); +} + +function setAdUnitMap(adUnitCode, auctionId, transactionId) { + if (!locals.adUnitMap[adUnitCode]) { + locals.adUnitMap[adUnitCode] = []; + } + + locals.adUnitMap[adUnitCode].push({ auctionId, transactionId }); +} + +/***************** + * BID_REQUESTED * + ****************/ +function onBidRequested({ auctionId, bids }) { + for (let { bidder, bidId, transactionId, src } of bids) { + const auction = locals.cache.auctions[auctionId]; + const adUnit = auction.adUnits.find(adUnit => adUnit.transactionId === transactionId); + if (!adUnit) return; + adUnit.bids.push({ + bidder, + bidId, + status: BidStatus.PENDING, + hasWon: 0, + source: src, + }); + + // if there is no manager for this auction, then the auction has already been completed + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].initiate(transactionId); + } +} + +/**************** + * BID_RESPONSE * + ***************/ +function onBidResponse({ requestId, auctionId, cpm, currency, originalCpm, floorData, mediaType, size, status, source }) { + const bid = getCachedBid(auctionId, requestId); + if (!bid) return; + + setBidStatus(bid, status); + Object.assign(bid, + { + bidResponse: { + cpm, + cur: currency, + cpmOrig: originalCpm, + cpmFloor: floorData?.floorValue, + mediaType, + size + }, + source + } + ); +} + +/**************** + * BID_REJECTED * + ***************/ +function onBidRejected({ requestId, auctionId, cpm, currency, originalCpm, floorData, mediaType, width, height, source }) { + const bid = getCachedBid(auctionId, requestId); + if (!bid) return; + + setBidStatus(bid, BidStatus.REJECTED); + Object.assign(bid, + { + bidResponse: { + cpm, + cur: currency, + cpmOrig: originalCpm, + cpmFloor: floorData?.floorValue, + mediaType, + size: `${width}x${height}` + }, + source + } + ); +} + +/*************** + * AUCTION_END * + **************/ +/** + * @param {Object} args + * @param {{requestId: string, status: string}[]} args.bidsReceived + * @param {string} args.auctionId + * @returns {void} + */ +function onAuctionEnd({ bidsReceived, auctionId }) { + for (let bid of bidsReceived) { + setCachedBidStatus(auctionId, bid.requestId, bid.status); + } +} + +/*********** + * BID_WON * + **********/ +function onBidWon(bidWon) { + const { auctionId, requestId, transactionId } = bidWon; + const bid = getCachedBid(auctionId, requestId); + if (!bid) { + return; + } + + setBidStatus(bid, bidWon.status ?? BidStatus.ERROR); + + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].complete(transactionId); +} + +/** + * @param {Bid} bid + * @param {BidStatus} [status] + * @returns {void} + */ +function setBidStatus(bid, status = BidStatus.AVAILABLE) { + const statusStates = { + pending: { + next: [BidStatus.AVAILABLE, BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + available: { + next: [BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + targetingSet: { + next: [BidStatus.RENDERED, BidStatus.ERROR, BidStatus.TIMEOUT], + }, + rendered: { + next: [], + }, + timeout: { + next: [], + }, + rejected: { + next: [], + }, + noBid: { + next: [], + }, + error: { + next: [BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + } + + const winningStatuses = [BidStatus.RENDERED]; + + if (statusStates[bid.status].next.includes(status)) { + bid.status = status; + if (winningStatuses.includes(status)) { + // occassionally we can detect a bidWon before prebid reports it as such + bid.hasWon = 1; + } + } +} + +function setCachedBidStatus(auctionId, bidId, status) { + const bid = getCachedBid(auctionId, bidId); + if (!bid) return; + setBidStatus(bid, status); +} + +/** + * Guarantees sending of data without waiting for response, even after page is left/closed + * + * @param {AnalyticsReport} report Request payload + * @param {string} endpoint URL + */ +function sendReport(report, endpoint) { + if (navigator.sendBeacon(endpoint, JSON.stringify(report))) { + log.info(`Analytics report sent to ${endpoint}`, report); + + return; + } + + log.error('Analytics report exceeded User-Agent data limits and was not sent.', report); +} + +/** + * Encapsulate certain logger functions and add a prefix to the final messages. + * + * @return {Object} New logger functions + */ +function getLogger() { + const LPREFIX = `${PROVIDER_NAME} Analytics: `; + + return { + info: (msg, ...args) => logInfo(`${LPREFIX}${msg}`, ...deepClone(args)), + warn: (msg, ...args) => logWarn(`${LPREFIX}${msg}`, ...deepClone(args)), + error: (msg, ...args) => logError(`${LPREFIX}${msg}`, ...deepClone(args)), + } +} diff --git a/modules/33acrossAnalyticsAdapter.md b/modules/33acrossAnalyticsAdapter.md new file mode 100644 index 00000000000..c56059e5526 --- /dev/null +++ b/modules/33acrossAnalyticsAdapter.md @@ -0,0 +1,76 @@ +# Overview + +```txt +Module Name: 33Across Analytics Adapter +Module Type: Analytics Adapter +Maintainer: analytics_support@33across.com +``` + +#### About + +This analytics adapter collects data about the performance of your ad slots +for each auction run on your site. It also provides insight into how identifiers +from the +[33Across User ID Sub-module](https://docs.prebid.org/dev-docs/modules/userid-submodules/33across.html) +and other user ID sub-modules improve your monetization. The data is sent at +the earliest opportunity for each auction to provide a more complete picture of +your ad performance. + +The analytics adapter is free to use! +However, the publisher must work with our account management team to obtain a +Publisher/Partner ID (PID) and enable Analytics for their account. +To get a PID and to have the publisher account enabled for Analytics, +you can reach out to our team at the following email - analytics_support@33across.com + +If you are an existing publisher and you already use a 33Across PID, +you can reach out to analytics_support@33across.com +to have your account enabled for analytics. + +The 33Across privacy policy is at . + +#### Analytics Options + +| Name | Scope | Example | Type | Description | +|-----------|----------|---------|----------|-------------| +| `pid` | required | abc123 | `string` | 33Across Publisher ID | +| `timeout` | optional | 10000 | `int` | Milliseconds to wait after last seen auction transaction before sending report (default 10000). | + +#### Configuration + +The data is sent at the earliest opportunity for each auction to provide +a more complete picture of your ad performance, even if the auction is interrupted +by a page navigation. At the latest, the adapter will always send the report +when the page is unloaded, at the end of the auction, or after the timeout, +whichever comes first. + +In order to guarantee consistent reports of your ad slot behavior, we recommend +including the GPT Pre-Auction Module, `gptPreAuction`. This module is included +by default when Prebid is downloaded. If you are compiling from source, +this might look something like: + +```sh +gulp bundle --modules=gptPreAuction,consentManagement,consentManagementGpp,consentManagementUsp,enrichmentFpdModule,gdprEnforcement,33acrossBidAdapter,33acrossIdSystem,33acrossAnalyticsAdapter +``` + +Enable the 33Across Analytics Adapter in Prebid.js using the analytics provider `33across` +and options as seen in the example below. + +#### Example Configuration + +```js +pbjs.enableAnalytics({ + provider: '33across', + options: { + /** + * The 33Across Publisher ID. + */ + pid: 'abc123', + /** + * Timeout in milliseconds after which an auction report + * will be sent regardless of auction state. + * [optional] + */ + timeout: 10000 + } +}); +``` diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index 26fc0a11fd5..0e9beb22013 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -6,7 +6,6 @@ import { getWindowTop, isArray, isGptPubadsDefined, - isSlotMatchingAdUnitCode, logInfo, logWarn, mergeDeep, @@ -14,6 +13,7 @@ import { uniques } from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; // **************************** UTILS *************************** // const BIDDER_CODE = '33across'; @@ -167,6 +167,7 @@ function buildRequests(bidRequests, bidderRequest) { ttxSettings, gdprConsent, uspConsent, + gppConsent, pageUrl, referer } = _buildRequestParams(bidRequests, bidderRequest); @@ -181,6 +182,7 @@ function buildRequests(bidRequests, bidderRequest) { bidRequests: groupedRequests[key], gdprConsent, uspConsent, + gppConsent, pageUrl, referer, ttxSettings, @@ -200,20 +202,15 @@ function _buildRequestParams(bidRequests, bidderRequest) { gdprApplies: false }, bidderRequest && bidderRequest.gdprConsent); - const uspConsent = bidderRequest && bidderRequest.uspConsent; - - const pageUrl = bidderRequest?.refererInfo?.page; - - const referer = bidderRequest?.refererInfo?.ref; - adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(uniques); return { ttxSettings, gdprConsent, - uspConsent, - pageUrl, - referer + uspConsent: bidderRequest?.uspConsent, + gppConsent: bidderRequest?.gppConsent, + pageUrl: bidderRequest?.refererInfo?.page, + referer: bidderRequest?.refererInfo?.ref } } @@ -247,10 +244,11 @@ function _getMRAKey(bidRequest) { } // Infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request -function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, pageUrl, referer, ttxSettings, bidderRequest }) { +function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, gppConsent = {}, pageUrl, referer, ttxSettings, bidderRequest }) { const ttxRequest = {}; const firstBidRequest = bidRequests[0]; const { siteId, test } = firstBidRequest.params; + const coppaValue = config.getConfig('coppa'); /* * Infer data for the request payload @@ -296,6 +294,17 @@ function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, pageU }); } + if (gppConsent.gppString) { + Object.assign(ttxRequest.regs, { + 'gpp': gppConsent.gppString, + 'gpp_sid': gppConsent.applicableSections + }); + } + + if (coppaValue !== undefined) { + ttxRequest.regs.coppa = Number(!!coppaValue); + } + ttxRequest.ext = { ttx: { prebidStartedAt: Date.now(), @@ -717,10 +726,10 @@ function _createBidResponse(bid, cur) { // Else no syncs // For logic on how we handle gdpr data see _createSyncs and module's unit tests // '33acrossBidAdapter#getUserSyncs' -function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { +function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncUrls = ( (syncOptions.iframeEnabled) - ? adapterState.uniqueSiteIds.map((siteId) => _createSync({ gdprConsent, uspConsent, siteId })) + ? adapterState.uniqueSiteIds.map((siteId) => _createSync({ gdprConsent, uspConsent, gppConsent, siteId })) : ([]) ); @@ -731,15 +740,16 @@ function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { } // Sync object will always be of type iframe for TTX -function _createSync({ siteId = 'zzz000000000003zzz', gdprConsent = {}, uspConsent }) { +function _createSync({ siteId = 'zzz000000000003zzz', gdprConsent = {}, uspConsent, gppConsent = {} }) { const ttxSettings = config.getConfig('ttxSettings'); const syncUrl = (ttxSettings && ttxSettings.syncUrl) || SYNC_ENDPOINT; const { consentString, gdprApplies } = gdprConsent; + const { gppString = '', applicableSections = [] } = gppConsent; const sync = { type: 'iframe', - url: `${syncUrl}&id=${siteId}&gdpr_consent=${encodeURIComponent(consentString)}&us_privacy=${encodeURIComponent(uspConsent)}` + url: `${syncUrl}&id=${siteId}&gdpr_consent=${encodeURIComponent(consentString)}&us_privacy=${encodeURIComponent(uspConsent)}&gpp=${encodeURIComponent(gppString)}&gpp_sid=${encodeURIComponent(applicableSections.join(','))}` }; if (typeof gdprApplies === 'boolean') { diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index be81b26b110..0f370237e21 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -8,7 +8,7 @@ import { logMessage, logError } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import { uspDataHandler } from '../src/adapterManager.js'; +import { uspDataHandler, coppaDataHandler, gppDataHandler } from '../src/adapterManager.js'; const MODULE_NAME = '33acrossId'; const API_URL = 'https://lexicon.33across.com/v1/envelope'; @@ -37,19 +37,30 @@ function getEnvelope(response) { function calculateQueryStringParams(pid, gdprConsentData) { const uspString = uspDataHandler.getConsentData(); const gdprApplies = Boolean(gdprConsentData?.gdprApplies); + const coppaValue = coppaDataHandler.getCoppa(); + const gppConsent = gppDataHandler.getConsentData(); + const params = { pid, gdpr: Number(gdprApplies), src: CALLER_NAME, - ver: '$prebid.version$' + ver: '$prebid.version$', + coppa: Number(coppaValue) }; if (uspString) { params.us_privacy = uspString; } - if (gdprApplies) { - params.gdpr_consent = gdprConsentData.consentString || ''; + if (gppConsent) { + const { gppString = '', applicableSections = [] } = gppConsent; + + params.gpp = gppString; + params.gpp_sid = encodeURIComponent(applicableSections.join(',')) + } + + if (gdprConsentData?.consentString) { + params.gdpr_consent = gdprConsentData.consentString; } return params; @@ -115,6 +126,15 @@ export const thirthyThreeAcrossIdSubmodule = { }, calculateQueryStringParams(pid, gdprConsentData), { method: 'GET', withCredentials: true }); } }; + }, + eids: { + '33acrossId': { + source: '33across.com', + atype: 1, + getValue: function(data) { + return data.envelope; + } + }, } }; diff --git a/modules/a1MediaBidAdapter.js b/modules/a1MediaBidAdapter.js new file mode 100644 index 00000000000..d640bbfe2d7 --- /dev/null +++ b/modules/a1MediaBidAdapter.js @@ -0,0 +1,104 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { replaceAuctionPrice } from '../src/utils.js'; + +const BIDDER_CODE = 'a1media'; +const END_POINT = 'https://d11.contentsfeed.com/dsp/breq/a1'; +const DEFAULT_CURRENCY = 'JPY'; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + if (!imp.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.bidfloorcur = bidRequest.params.currency || DEFAULT_CURRENCY; + } + if (bidRequest.params.battr) { + Object.keys(bidRequest.mediaTypes).forEach(mType => { + imp[mType].battr = bidRequest.params.battr; + }) + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + if (!request.cur) { + request.cur = [bid.params.currency || DEFAULT_CURRENCY]; + } + if (bid.params.bcat) { + request.bcat = bid.params.bcat; + } + return request; + }, + bidResponse(buildBidResponse, bid, context) { + const { bidRequest } = context; + + let resMediaType; + const reqMediaTypes = Object.keys(bidRequest.mediaTypes); + if (reqMediaTypes.length === 1) { + resMediaType = reqMediaTypes[0]; + } else { + if (bid.adm.search(/^(<\?xml| { + const parsedBid = seatbidItem.bid.map((bidItem) => ({ + ...bidItem, + adm: replaceAuctionPrice(bidItem.adm, bidItem.price), + nurl: replaceAuctionPrice(bidItem.nurl, bidItem.price) + })); + return {...seatbidItem, bid: parsedBid}; + }); + + const responseBody = {...serverResponse.body, seatbid: parsedSeatbid}; + const bids = converter.fromORTB({ + response: responseBody, + request: bidRequest.data, + }).bids; + return bids; + }, + +}; +registerBidder(spec); diff --git a/modules/a1MediaBidAdapter.md b/modules/a1MediaBidAdapter.md new file mode 100644 index 00000000000..304b7e1bb5a --- /dev/null +++ b/modules/a1MediaBidAdapter.md @@ -0,0 +1,93 @@ +# Overview + +```markdown +Module Name: A1Media Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@a1mediagroup.co.kr +``` + +# Description + +Connects to A1Media exchange for bids. + +# Test Parameters + +## Sample Banner Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[320, 100]], + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` + +## Sample Video Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + mimes: ['video/mp4'], + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` + +## Sample Native Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + native: { + title: { + len: 140 + }, + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` diff --git a/modules/a1MediaRtdProvider.js b/modules/a1MediaRtdProvider.js new file mode 100644 index 00000000000..9fa6b307b6a --- /dev/null +++ b/modules/a1MediaRtdProvider.js @@ -0,0 +1,92 @@ +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { isEmptyStr, mergeDeep } from '../src/utils.js'; + +const REAL_TIME_MODULE = 'realTimeData'; +const MODULE_NAME = 'a1Media'; +const SCRIPT_URL = 'https://linkback.contentsfeed.com/src'; +export const A1_SEG_KEY = '__a1tg'; +export const A1_AUD_KEY = 'a1_gid'; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME}); + +/** @type {RtdSubmodule} */ +export const subModuleObj = { + name: MODULE_NAME, + init: init, + getBidRequestData: alterBidRequests, +}; + +export function getStorageData(key) { + let storageValue = ''; + if (storage.getDataFromLocalStorage(key)) { + storageValue = storage.getDataFromLocalStorage(key); + } else if (storage.getCookie(key)) { + storageValue = storage.getCookie(key); + } + return storageValue; +} + +function loadLbScript(tagname) { + const linkback = window.linkback = window.linkback || {}; + if (!linkback.l) { + linkback.l = true; + + const scriptUrl = `${SCRIPT_URL}/${tagname}`; + loadExternalScript(scriptUrl, MODULE_NAME); + } +} + +function init(config, userConsent) { + const tagId = config.params.tagId; + if (tagId && !isEmptyStr(tagId)) { + loadLbScript(config.params.tagId); + return true; + } + if (!isEmptyStr(getStorageData(A1_SEG_KEY))) { + return true; + } + return false; +} + +function alterBidRequests(reqBidsConfigObj, callback, config, userConsent) { + const a1seg = getStorageData(A1_SEG_KEY); + const a1gid = getStorageData(A1_AUD_KEY); + + const a1UserSegData = { + name: 'a1mediagroup.com', + ext: { + segtax: 900 + }, + segment: a1seg.split(',').map(x => ({id: x})) + }; + + const a1UserEid = { + source: 'a1mediagroup.com', + uids: [ + { + id: a1gid, + atype: 1 + } + ] + }; + + const a1Ortb2 = { + user: { + data: [ + a1UserSegData + ], + ext: { + eids: [ + a1UserEid + ] + } + } + }; + mergeDeep(reqBidsConfigObj.ortb2Fragments.global, a1Ortb2); + callback(); +} + +submodule(REAL_TIME_MODULE, subModuleObj); diff --git a/modules/a1MediaRtdProvider.md b/modules/a1MediaRtdProvider.md new file mode 100644 index 00000000000..ab2077ebbbb --- /dev/null +++ b/modules/a1MediaRtdProvider.md @@ -0,0 +1,46 @@ +# Overview + +Module Name: A1Media Rtd Provider +Module Type: Rtd Provider +Maintainer: dev@a1mediagroup.co.kr + +# Description + +This module loads external code using the passed parameter (params.tagId). + +The A1Media RTD module loads A1Media script for obtains user segments, and provides user segment data to bid-requests.
+to get user segments, you will need a1media script customized for site. + +To use this module, you’ll need to work with [A1MediaGroup](https://www.a1mediagroup.com/) to get an account and receive instructions on how to set up your pages and ad server. + +Contact dev@a1mediagroup.co.kr for information. + +### Integration + +1) Build the A1Media RTD Module into the Prebid.js package with: + +``` +gulp build --modules=a1MediaRtdProvider,... +``` + +2) Use `setConfig` to instruct Prebid.js to initilaize the A1Media RTD module, as specified below. + +### Configuration + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: "a1Media", + waitForIt: true, + params: { + // 'tagId' is unique value for each account. + tagId: 'lb4test' + } + } + ] + } +}); +``` diff --git a/modules/acuityAdsBidAdapter.js b/modules/acuityadsBidAdapter.js similarity index 94% rename from modules/acuityAdsBidAdapter.js rename to modules/acuityadsBidAdapter.js index b0bb132ddae..5b12eb2133b 100644 --- a/modules/acuityAdsBidAdapter.js +++ b/modules/acuityadsBidAdapter.js @@ -153,6 +153,15 @@ export const spec = { tmax: bidderRequest.timeout }; + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + const len = validBidRequests.length; for (let i = 0; i < len; i++) { const bid = validBidRequests[i]; diff --git a/modules/acuityAdsBidAdapter.md b/modules/acuityadsBidAdapter.md similarity index 98% rename from modules/acuityAdsBidAdapter.md rename to modules/acuityadsBidAdapter.md index a19e0a6b0ba..7f001cd9376 100644 --- a/modules/acuityAdsBidAdapter.md +++ b/modules/acuityadsBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: AcuityAds Bidder Adapter Module Type: AcuityAds Bidder Adapter -Maintainer: sa-support@brightcom.com +Maintainer: rafi.babler@acuityads.com ``` # Description diff --git a/modules/adWMGBidAdapter.js b/modules/adWMGBidAdapter.js index 36935e80d3b..d268c4cafa8 100644 --- a/modules/adWMGBidAdapter.js +++ b/modules/adWMGBidAdapter.js @@ -1,9 +1,9 @@ 'use strict'; -import { tryAppendQueryString } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'adWMG'; const ENDPOINT = 'https://hb.adwmg.com/hb'; diff --git a/modules/adagioAnalyticsAdapter.js b/modules/adagioAnalyticsAdapter.js index 96f3f089cbd..9c4c0e8fea7 100644 --- a/modules/adagioAnalyticsAdapter.js +++ b/modules/adagioAnalyticsAdapter.js @@ -5,16 +5,44 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { getWindowTop } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getWindowTop, getWindowSelf, deepAccess, logInfo, logError } from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; const events = Object.keys(CONSTANTS.EVENTS).map(key => CONSTANTS.EVENTS[key]); -const VERSION = '2.0.0'; +const ADAGIO_GVLID = 617; +const VERSION = '3.0.0'; +const PREBID_VERSION = '$prebid.version$'; +const ENDPOINT = 'https://c.4dex.io/pba.gif'; +const cache = { + auctions: {}, + getAuction: function(auctionId, adUnitCode) { + return this.auctions[auctionId][adUnitCode]; + }, + updateAuction: function(auctionId, adUnitCode, values) { + this.auctions[auctionId][adUnitCode] = { + ...this.auctions[auctionId][adUnitCode], + ...values + }; + }, -const adagioEnqueue = function adagioEnqueue(action, data) { - getWindowTop().ADAGIO.queue.push({ action, data, ts: Date.now() }); -} + // Map prebid auction id to adagio auction id + auctionIdReferences: {}, + addPrebidAuctionIdRef(auctionId, adagioAuctionId) { + this.auctionIdReferences[auctionId] = adagioAuctionId; + }, + getAdagioAuctionId(auctionId) { + return this.auctionIdReferences[auctionId]; + } +}; +const enc = window.encodeURIComponent; + +/** +/* BEGIN ADAGIO.JS CODE +*/ function canAccessTopWindow() { try { @@ -24,12 +52,312 @@ function canAccessTopWindow() { } catch (error) { return false; } +}; + +function getCurrentWindow() { + return currentWindow; +}; + +let currentWindow; + +const adagioEnqueue = function adagioEnqueue(action, data) { + getCurrentWindow().ADAGIO.queue.push({ action, data, ts: Date.now() }); +}; + +/** +* END ADAGIO.JS CODE +*/ + +/** +* UTILS FUNCTIONS +*/ + +const guard = { + adagio: (value) => isAdagio(value), + bidTracked: (auctionId, adUnitCode) => deepAccess(cache, `auctions.${auctionId}.${adUnitCode}`, false) +}; + +function removeDuplicates(arr, getKey) { + const seen = {}; + return arr.filter(item => { + const key = getKey(item); + return seen.hasOwnProperty(key) ? false : (seen[key] = true); + }); +}; + +function getAdapterNameForAlias(aliasName) { + return adapterManager.aliasRegistry[aliasName] || aliasName; +}; + +function isAdagio(value) { + return value.toLowerCase().includes('adagio') || + getAdapterNameForAlias(value).toLowerCase().includes('adagio'); +}; + +function getMediaTypeAlias(mediaType) { + const mediaTypesMap = { + banner: 'ban', + outstream: 'vidout', + instream: 'vidin', + adpod: 'vidadpod', + native: 'nat' + }; + return mediaTypesMap[mediaType] || mediaType; +}; + +/** +* sendRequest to Adagio. It filter null values and encode each query param. +* @param {Object} qp +*/ +function sendRequest(qp) { + // Removing null values + qp = Object.keys(qp).reduce((acc, key) => { + if (qp[key] !== null) { + acc[key] = qp[key]; + } + return acc; + }, {}); + + const url = `${ENDPOINT}?${Object.keys(qp).map(key => `${key}=${enc(qp[key])}`).join('&')}`; + ajax(url, null, null, {method: 'GET'}); +}; + +/** + * Send a new beacon to Adagio. It increment the version of the beacon. + * @param {string} auctionId + * @param {string} adUnitCode + */ +function sendNewBeacon(auctionId, adUnitCode) { + cache.updateAuction(auctionId, adUnitCode, { + v: (cache.getAuction(auctionId, adUnitCode).v || 0) + 1 + }); + sendRequest(cache.getAuction(auctionId, adUnitCode)); +}; + +function getTargetedAuctionId(bid) { + return deepAccess(bid, 'latestTargetedAuctionId') || deepAccess(bid, 'auctionId'); } +/** + * END UTILS FUNCTIONS +*/ + +/** + * HANDLERS + * - handlerAuctionInit + * - handlerBidResponse + * - handlerBidWon + * - handlerAdRender + * + * Each handler is called when the event is fired. +*/ + +function handlerAuctionInit(event) { + const w = getCurrentWindow(); + + const prebidAuctionId = event.auctionId; + const adUnitCodes = removeDuplicates(event.adUnitCodes, adUnitCode => adUnitCode); + + // Check if Adagio is on the bid requests. + // If not, we don't need to track the auction. + const adagioBidRequest = event.bidderRequests.find(bidRequest => isAdagio(bidRequest.bidderCode)); + if (!adagioBidRequest) { + logInfo(`Adagio is not on the bid requests for auction '${prebidAuctionId}'`) + return; + } + + cache.auctions[prebidAuctionId] = {}; + + adUnitCodes.forEach(adUnitCode => { + const adUnits = event.adUnits.filter(adUnit => adUnit.code === adUnitCode); + + // Get all bidders configures for the ad unit. + const bidders = removeDuplicates( + adUnits.map(adUnit => adUnit.bids.map(bid => ({bidder: bid.bidder, params: bid.params}))).flat(), + bidder => bidder.bidder + ); + + // Check if Adagio is configured for the ad unit. + // If not, we don't need to track the ad unit. + const adagioBidder = bidders.find(bidder => isAdagio(bidder.bidder)); + if (!adagioBidder) { + logInfo(`Adagio is not configured for ad unit '${adUnitCode}'`); + return; + } + + // Get all media types and banner sizes configured for the ad unit. + const mediaTypes = adUnits.map(adUnit => adUnit.mediaTypes); + const mediaTypesKeys = removeDuplicates( + mediaTypes.map(mediaTypeObj => Object.keys(mediaTypeObj)).flat(), + mediaTypeKey => mediaTypeKey + ).map(mediaType => getMediaTypeAlias(mediaType)).sort(); + const bannerSizes = removeDuplicates( + mediaTypes.filter(mediaType => mediaType.hasOwnProperty(BANNER)) + .map(mediaType => mediaType[BANNER].sizes.map(size => size.join('x'))) + .flat(), + bannerSize => bannerSize + ).sort(); + + // Get all Adagio bids for the ad unit from the bidRequest. + // If no bids, we don't need to track the ad unit. + const adagioAdUnitBids = adagioBidRequest.bids.filter(bid => bid.adUnitCode === adUnitCode); + if (deepAccess(adagioAdUnitBids, 'length', 0) <= 0) { + logInfo(`Adagio is not on the bid requests for ad unit '${adUnitCode}' and auction '${prebidAuctionId}'`) + return; + } + // Get Adagio params from the first bid. + // We assume that all Adagio bids for a same adunit have the same params. + const params = adagioAdUnitBids[0].params; + + const adagioAuctionId = params.adagioAuctionId; + cache.addPrebidAuctionIdRef(prebidAuctionId, adagioAuctionId); + + // Get all media types requested for Adagio. + const adagioMediaTypes = removeDuplicates( + adagioAdUnitBids.map(bid => Object.keys(bid.mediaTypes)).flat(), + mediaTypeKey => mediaTypeKey + ).flat().map(mediaType => getMediaTypeAlias(mediaType)).sort(); + + const qp = { + v: 0, + pbjsv: PREBID_VERSION, + org_id: params.organizationId, + site: params.site, + pv_id: params.pageviewId, + auct_id: adagioAuctionId, + adu_code: adUnitCode, + url_dmn: w.location.hostname, + dvc: params.environment, + pgtyp: params.pagetype, + plcmt: params.placement, + tname: params.testName || null, + tvname: params.testVariationName || null, + mts: mediaTypesKeys.join(','), + ban_szs: bannerSizes.join(','), + bdrs: bidders.map(bidder => getAdapterNameForAlias(bidder.bidder)).sort().join(','), + adg_mts: adagioMediaTypes.join(',') + }; + + cache.auctions[prebidAuctionId][adUnitCode] = qp; + sendNewBeacon(prebidAuctionId, adUnitCode); + }); +}; + +/** + * handlerBidResponse allow to track the adagio bid response + * and to update the auction cache with the seat ID. + * No beacon is sent here. +*/ +function handlerBidResponse(event) { + if (!guard.adagio(event.bidder)) { + return; + } + + if (!guard.bidTracked(event.auctionId, event.adUnitCode)) { + return; + } + + cache.updateAuction(event.auctionId, event.adUnitCode, { + adg_sid: event.seatId || null + }); +}; + +function handlerBidWon(event) { + let auctionId = getTargetedAuctionId(event); + + if (!guard.bidTracked(auctionId, event.adUnitCode)) { + return; + } + + let adsCurRateToUSD = (event.currency === 'USD') ? 1 : null; + let ogCurRateToUSD = (event.originalCurrency === 'USD') ? 1 : null; + try { + if (typeof getGlobal().convertCurrency === 'function') { + // Currency module is loaded, we can calculate the conversion rate. + + // Get the conversion rate from the original currency to USD. + ogCurRateToUSD = getGlobal().convertCurrency(1, event.originalCurrency, 'USD'); + // Get the conversion rate from the ad server currency to USD. + adsCurRateToUSD = getGlobal().convertCurrency(1, event.currency, 'USD'); + } + } catch (error) { + logError('Error on Adagio Analytics Adapter - handlerBidWon', error); + } + + const adagioAuctionCacheId = ( + (event.latestTargetedAuctionId && event.latestTargetedAuctionId !== event.auctionId) + ? cache.getAdagioAuctionId(event.auctionId) + : null); + + cache.updateAuction(auctionId, event.adUnitCode, { + win_bdr: getAdapterNameForAlias(event.bidder), + win_mt: getMediaTypeAlias(event.mediaType), + win_ban_sz: event.mediaType === BANNER ? `${event.width}x${event.height}` : null, + + // ad server currency + win_cpm: event.cpm, + cur: event.currency, + cur_rate: adsCurRateToUSD, + + // original currency from bidder + og_cpm: event.originalCpm, + og_cur: event.originalCurrency, + og_cur_rate: ogCurRateToUSD, + + // cache bid id + auct_id_c: adagioAuctionCacheId, + }); + sendNewBeacon(auctionId, event.adUnitCode); +}; + +function handlerAdRender(event, isSuccess) { + const { adUnitCode } = event.bid; + let auctionId = getTargetedAuctionId(event.bid); + + if (!guard.bidTracked(auctionId, adUnitCode)) { + return; + } + + cache.updateAuction(auctionId, adUnitCode, { + rndr: isSuccess ? 1 : 0 + }); + sendNewBeacon(auctionId, adUnitCode); +}; + +/** + * END HANDLERS +*/ + let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { - track: function({ eventType, args }) { - if (typeof args !== 'undefined' && events.indexOf(eventType) !== -1) { - adagioEnqueue('pb-analytics-event', { eventName: eventType, args }); + track: function(event) { + const { eventType, args } = event; + + try { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + handlerAuctionInit(args); + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + handlerBidResponse(args); + break; + case CONSTANTS.EVENTS.BID_WON: + handlerBidWon(args); + break; + case CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED: + case CONSTANTS.EVENTS.AD_RENDER_FAILED: + handlerAdRender(args, eventType === CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED); + break; + } + } catch (error) { + logError('Error on Adagio Analytics Adapter', error); + } + + try { + if (typeof args !== 'undefined' && events.indexOf(eventType) !== -1) { + adagioEnqueue('pb-analytics-event', { eventName: eventType, args }); + } + } catch (error) { + logError('Error on Adagio Analytics Adapter - adagio.js', error); } } }); @@ -37,11 +365,8 @@ let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { adagioAdapter.originEnableAnalytics = adagioAdapter.enableAnalytics; adagioAdapter.enableAnalytics = config => { - if (!canAccessTopWindow()) { - return; - } - - const w = getWindowTop(); + const w = (canAccessTopWindow()) ? getWindowTop() : getWindowSelf(); + currentWindow = w; w.ADAGIO = w.ADAGIO || {}; w.ADAGIO.queue = w.ADAGIO.queue || []; @@ -53,7 +378,8 @@ adagioAdapter.enableAnalytics = config => { adapterManager.registerAnalyticsAdapter({ adapter: adagioAdapter, - code: 'adagio' + code: 'adagio', + gvlid: ADAGIO_GVLID, }); export default adagioAdapter; diff --git a/modules/adagioAnalyticsAdapter.md b/modules/adagioAnalyticsAdapter.md index 312a26ea8da..9fc2cb0bb88 100644 --- a/modules/adagioAnalyticsAdapter.md +++ b/modules/adagioAnalyticsAdapter.md @@ -8,10 +8,10 @@ Maintainer: dev@adagio.io Analytics adapter for Adagio -# Test Parameters +# Settings -``` -{ - provider: 'adagio' -} +```js + pbjs.enableAnalytics({ + provider: 'adagio', + }); ``` diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 57dd80aa9b1..90f62ba4694 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -1,12 +1,10 @@ import {find} from '../src/polyfill.js'; import { - _map, cleanObj, deepAccess, deepClone, generateUUID, getDNT, - getGptSlotInfoForAdUnitCode, getUniqueIdentifierStr, getWindowSelf, getWindowTop, @@ -34,6 +32,7 @@ import {OUTSTREAM} from '../src/video.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import { userSync } from '../src/userSync.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'adagio'; const LOG_PREFIX = 'Adagio:'; @@ -54,8 +53,9 @@ const ADAGIO_PUBKEY = 'AL16XT44Sfp+8SHVF1UdC7hydPSMVLMhsYknKDdwqq+0ToDSJrP0+Qh0k const ADAGIO_PUBKEY_E = 65537; const CURRENCY = 'USD'; -// This provide a whitelist and a basic validation of OpenRTB 2.6 options used by the Adagio SSP. -// https://iabtechlab.com/wp-content/uploads/2022/04/OpenRTB-2-6_FINAL.pdf +// This provide a whitelist and a basic validation of OpenRTB 2.5 options used by the Adagio SSP. +// Accept all options but 'protocol', 'companionad', 'companiontype', 'ext' +// https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf export const ORTB_VIDEO_PARAMS = { 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), 'minduration': (value) => isInteger(value), @@ -399,6 +399,17 @@ function _getUspConsent(bidderRequest) { return (deepAccess(bidderRequest, 'uspConsent')) ? { uspConsent: bidderRequest.uspConsent } : false; } +function _getGppConsent(bidderRequest) { + let gpp = deepAccess(bidderRequest, 'gppConsent.gppString') + let gppSid = deepAccess(bidderRequest, 'gppConsent.applicableSections') + + if (!gpp || !gppSid) { + gpp = deepAccess(bidderRequest, 'ortb2.regs.gpp', '') + gppSid = deepAccess(bidderRequest, 'ortb2.regs.gpp_sid', []) + } + return { gpp, gppSid } +} + function _getSchain(bidRequest) { return deepAccess(bidRequest, 'schain'); } @@ -558,6 +569,7 @@ function _parseNativeBidResponse(bid) { bid.native = native } +// bidRequest param must be the `bidRequest` object with the original `auctionId` value. function _getFloors(bidRequest) { if (!isFn(bidRequest.getFloor)) { return false; @@ -976,6 +988,7 @@ export const spec = { const gdprConsent = _getGdprConsent(bidderRequest) || {}; const uspConsent = _getUspConsent(bidderRequest) || {}; const coppa = _getCoppa(); + const gppConsent = _getGppConsent(bidderRequest) const schain = _getSchain(validBidRequests[0]); const eids = _getEids(validBidRequests[0]) || []; const syncEnabled = deepAccess(config.getConfig('userSync'), 'syncEnabled') @@ -983,8 +996,8 @@ export const spec = { const aucId = generateUUID() - const adUnits = _map(validBidRequests, (rawBidRequest) => { - const bidRequest = {...rawBidRequest} + const adUnits = validBidRequests.map(rawBidRequest => { + const bidRequest = deepClone(rawBidRequest); // Fix https://github.com/prebid/Prebid.js/issues/9781 bidRequest.auctionId = aucId @@ -1055,7 +1068,10 @@ export const spec = { }); // Handle priceFloors module - const computedFloors = _getFloors(bidRequest); + // We need to use `rawBidRequest` as param because: + // - adagioBidAdapter generates its own auctionId due to transmitTid activity limitation (see https://github.com/prebid/Prebid.js/pull/10079) + // - the priceFloors.getFloor() uses a `_floorDataForAuction` map to store the floors based on the auctionId. + const computedFloors = _getFloors(rawBidRequest); if (isArray(computedFloors) && computedFloors.length) { bidRequest.floors = computedFloors @@ -1118,8 +1134,8 @@ export const spec = { // remove useless props delete adUnitCopy.floorData; delete adUnitCopy.params.siteId; - delete adUnitCopy.userId - delete adUnitCopy.userIdAsEids + delete adUnitCopy.userId; + delete adUnitCopy.userIdAsEids; groupedAdUnits[adUnitCopy.params.organizationId] = groupedAdUnits[adUnitCopy.params.organizationId] || []; groupedAdUnits[adUnitCopy.params.organizationId].push(adUnitCopy); @@ -1127,13 +1143,20 @@ export const spec = { return groupedAdUnits; }, {}); + // Adding more params on the original bid object. + // Those params are not sent to the server. + // They are used for further operations on analytics adapter. + validBidRequests.forEach(rawBidRequest => { + rawBidRequest.params.adagioAuctionId = aucId + rawBidRequest.params.pageviewId = pageviewId + }); + // Build one request per organizationId - const requests = _map(Object.keys(groupedAdUnits), organizationId => { + const requests = Object.keys(groupedAdUnits).map(organizationId => { return { method: 'POST', url: ENDPOINT, data: { - id: generateUUID(), organizationId: organizationId, secure: secure, device: device, @@ -1144,7 +1167,9 @@ export const spec = { regs: { gdpr: gdprConsent, coppa: coppa, - ccpa: uspConsent + ccpa: uspConsent, + gpp: gppConsent.gpp, + gppSid: gppConsent.gppSid }, schain: schain, user: { @@ -1152,7 +1177,8 @@ export const spec = { }, prebidVersion: '$prebid.version$', featuresVersion: FEATURES_VERSION, - usIfr: usIfr + usIfr: usIfr, + adgjs: storage.localStorageIsEnabled() }, options: { contentType: 'text/plain' diff --git a/modules/adagioBidAdapter.md b/modules/adagioBidAdapter.md index 45f39fc6f2d..19673571982 100644 --- a/modules/adagioBidAdapter.md +++ b/modules/adagioBidAdapter.md @@ -107,10 +107,11 @@ var adUnits = [ cpm: 3.00 // default to 1.00 }, video: { - api: [2, 7], // Required - Your video player must at least support the value 2 and/or 7. + api: [2], // Required - Your video player must at least support the value 2 playbackMethod: [6], // Highly recommended skip: 0 - // OpenRTB video options defined here override ones defined in mediaTypes. + // OpenRTB 2.5 video options defined here override ones defined in mediaTypes. + // Not supported: 'protocol', 'companionad', 'companiontype', 'ext' }, native: { // Optional OpenRTB Native 1.2 request object. Only `context`, `plcmttype` fields are supported. @@ -193,6 +194,8 @@ If the FPD value is an array, the 1st value of this array will be used. placement: 'in_article', adUnitElementId: 'article_outstream', video: { + api: [2], + playbackMethod: [6], skip: 0 }, debug: { diff --git a/modules/adfBidAdapter.js b/modules/adfBidAdapter.js index 5c4b03c3bb2..e5b40f66176 100644 --- a/modules/adfBidAdapter.js +++ b/modules/adfBidAdapter.js @@ -69,8 +69,11 @@ export const spec = { bid.netRevenue = pt; const floorInfo = bid.getFloor ? bid.getFloor({ - currency: currency || 'USD' + currency: currency || 'USD', + size: '*', + mediaType: '*' }) : {}; + const bidfloor = floorInfo.floor; const bidfloorcur = floorInfo.currency; const { mid, inv, mname } = bid.params; diff --git a/modules/adfusionBidAdapter.js b/modules/adfusionBidAdapter.js new file mode 100644 index 00000000000..b3638159c2a --- /dev/null +++ b/modules/adfusionBidAdapter.js @@ -0,0 +1,90 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const adpterVersion = '1.0'; +export const REQUEST_URL = 'https://spicyrtb.com/auction/prebid'; + +export const spec = { + code: 'adfusion', + gvlid: 844, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + isBannerBid, + isVideoBid, +}; + +registerBidder(spec); + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + utils.mergeDeep(req, { + at: 1, + ext: { + prebid: { + accountid: bid.params.accountId, + adapterVersion: `${adpterVersion}`, + }, + }, + }); + return req; + }, + response(buildResponse, bidResponses, ortbResponse, context) { + const response = buildResponse(bidResponses, ortbResponse, context); + return response.bids; + }, +}); + +function isBidRequestValid(bidRequest) { + const isValid = bidRequest.params.accountId; + if (!isValid) { + utils.logError('AdFusion adapter bidRequest has no accountId'); + return false; + } + return true; +} + +function buildRequests(bids, bidderRequest) { + let videoBids = bids.filter((bid) => isVideoBid(bid)); + let bannerBids = bids.filter((bid) => isBannerBid(bid)); + let requests = bannerBids.length + ? [createRequest(bannerBids, bidderRequest, BANNER)] + : []; + videoBids.forEach((bid) => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + return requests; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + return { + method: 'POST', + url: REQUEST_URL, + data: converter.toORTB({ + bidRequests, + bidderRequest, + context: { mediaType }, + }), + }; +} + +function isVideoBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner'); +} + +function interpretResponse(resp, req) { + return converter.fromORTB({ request: req.data, response: resp.body }); +} diff --git a/modules/adfusionBidAdapter.md b/modules/adfusionBidAdapter.md new file mode 100644 index 00000000000..803a03ba1a1 --- /dev/null +++ b/modules/adfusionBidAdapter.md @@ -0,0 +1,61 @@ +# Overview + +``` +Module Name: AdFusion Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@adfusion.pl +``` + +# Description + +Module that connects to AdFusion demand sources + +# Banner Test Parameters + +```js +var adUnits = [ + { + code: "test-banner", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + [320, 480], + ], + }, + }, + bids: [ + { + bidder: "adfusion", + params: { + accountId: 1234, // required + }, + }, + ], + }, +]; +``` + +# Video Test Parameters + +```js +var videoAdUnit = { + code: "video1", + mediaTypes: { + video: { + context: "instream", + playerSize: [640, 480], + mimes: ["video/mp4"], + }, + }, + bids: [ + { + bidder: "adfusion", + params: { + accountId: 1234, // required + }, + }, + ], +}; +``` diff --git a/modules/adgenerationBidAdapter.js b/modules/adgenerationBidAdapter.js index f18b7629d8f..b40378c8e35 100644 --- a/modules/adgenerationBidAdapter.js +++ b/modules/adgenerationBidAdapter.js @@ -1,8 +1,10 @@ -import {tryAppendQueryString, getBidIdParameter, escapeUnsafeChars, deepAccess} from '../src/utils.js'; +import {deepAccess, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; +import {escapeUnsafeChars} from '../libraries/htmlEscape/htmlEscape.js'; const ADG_BIDDER_CODE = 'adgeneration'; @@ -28,7 +30,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const ADGENE_PREBID_VERSION = '1.6.0'; + const ADGENE_PREBID_VERSION = '1.6.2'; let serverRequests = []; for (let i = 0, len = validBidRequests.length; i < len; i++) { const validReq = validBidRequests[i]; @@ -41,6 +43,7 @@ export const spec = { const imuid = deepAccess(validReq, 'userId.imuid'); const gpid = deepAccess(validReq, 'ortb2Imp.ext.gpid'); const sua = deepAccess(validReq, 'ortb2.device.sua'); + const uid2 = deepAccess(validReq, 'userId.uid2.id'); let data = ``; data = tryAppendQueryString(data, 'posall', 'SSPLOC'); const id = getBidIdParameter('id', validReq.params); @@ -58,10 +61,10 @@ export const spec = { data = tryAppendQueryString(data, 'adgext_id5_id', id5id); data = tryAppendQueryString(data, 'adgext_id5_id_link_type', id5LinkType); data = tryAppendQueryString(data, 'adgext_imuid', imuid); - data = tryAppendQueryString(data, 'adgext_uid2', validReq.userId ? validReq.userId.uid2 : null); - data = tryAppendQueryString(data, 'gpid', gpid ? encodeURIComponent(gpid) : null); + data = tryAppendQueryString(data, 'adgext_uid2', uid2); + data = tryAppendQueryString(data, 'gpid', gpid); data = tryAppendQueryString(data, 'uach', sua ? JSON.stringify(sua) : null); - data = tryAppendQueryString(data, 'schain', validReq.schain ? encodeURIComponent(JSON.stringify(validReq.schain)) : null); + data = tryAppendQueryString(data, 'schain', validReq.schain ? JSON.stringify(validReq.schain) : null); // native以外にvideo等の対応が入った場合は要修正 if (!validReq.mediaTypes || !validReq.mediaTypes.native) { diff --git a/modules/adkernelAdnBidAdapter.js b/modules/adkernelAdnBidAdapter.js index 4db46aca3c6..81067a3efcf 100644 --- a/modules/adkernelAdnBidAdapter.js +++ b/modules/adkernelAdnBidAdapter.js @@ -159,7 +159,7 @@ export const spec = { code: 'adkernelAdn', gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], - aliases: ['engagesimply'], + aliases: ['engagesimply', 'adpluto_dsp'], isBidRequestValid: function(bidRequest) { return 'params' in bidRequest && diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 64567832dbd..9d9da8cb0ab 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -5,7 +5,6 @@ import { createTrackPixelHtml, deepAccess, deepSetValue, - getAdUnitSizes, getDefinedParams, getDNT, isArray, @@ -22,6 +21,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {find} from '../src/polyfill.js'; import {config} from '../src/config.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; /* * In case you're AdKernel whitelable platform's client who needs branded adapter to @@ -29,18 +29,19 @@ import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; * * Please contact prebid@adkernel.com and we'll add your adapter as an alias. */ -const VIDEO_PARAMS = ['pos', 'context', 'placement', 'api', 'mimes', 'protocols', 'playbackmethod', 'minduration', 'maxduration', +const VIDEO_PARAMS = ['pos', 'context', 'placement', 'plcmt', 'api', 'mimes', 'protocols', 'playbackmethod', 'minduration', 'maxduration', 'startdelay', 'linearity', 'skip', 'skipmin', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackend', 'boxingallowed']; const VIDEO_FPD = ['battr', 'pos']; const NATIVE_FPD = ['battr', 'api']; +const BANNER_PARAMS = ['pos']; const BANNER_FPD = ['btype', 'battr', 'pos', 'api']; const VERSION = '1.6'; const SYNC_IFRAME = 1; const SYNC_IMAGE = 2; -const SYNC_TYPES = Object.freeze({ +const SYNC_TYPES = { 1: 'iframe', 2: 'image' -}); +}; const GVLID = 14; const NATIVE_MODEL = [ @@ -98,9 +99,10 @@ export const spec = { {code: 'displayioads'}, {code: 'rtbdemand_com'}, {code: 'bidbuddy'}, - {code: 'adliveconnect'}, {code: 'didnadisplay'}, - {code: 'qortex'} + {code: 'qortex'}, + {code: 'adpluto'}, + {code: 'headbidder'} ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -274,33 +276,37 @@ function buildImp(bidRequest, secure) { var mediaType; var sizes = []; - if (deepAccess(bidRequest, 'mediaTypes.banner')) { + if (bidRequest.mediaTypes?.banner) { sizes = getAdUnitSizes(bidRequest); + let pbBanner = bidRequest.mediaTypes.banner; imp.banner = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, BANNER_FPD), + ...getDefinedParamsOrEmpty(pbBanner, BANNER_PARAMS), format: sizes.map(wh => parseGPTSingleSizeArrayToRtbSize(wh)), topframe: 0 }; - populateImpFpd(imp.banner, bidRequest, BANNER_FPD); mediaType = BANNER; - } else if (deepAccess(bidRequest, 'mediaTypes.video')) { - let video = deepAccess(bidRequest, 'mediaTypes.video'); - imp.video = getDefinedParams(video, VIDEO_PARAMS); - populateImpFpd(imp.video, bidRequest, VIDEO_FPD); - if (video.playerSize) { - sizes = video.playerSize[0]; + } else if (bidRequest.mediaTypes?.video) { + let pbVideo = bidRequest.mediaTypes.video; + imp.video = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, VIDEO_FPD), + ...getDefinedParamsOrEmpty(pbVideo, VIDEO_PARAMS) + }; + if (pbVideo.playerSize) { + sizes = pbVideo.playerSize[0]; imp.video = Object.assign(imp.video, parseGPTSingleSizeArrayToRtbSize(sizes) || {}); - } else if (video.w && video.h) { - imp.video.w = video.w; - imp.video.h = video.h; + } else if (pbVideo.w && pbVideo.h) { + imp.video.w = pbVideo.w; + imp.video.h = pbVideo.h; } mediaType = VIDEO; - } else if (deepAccess(bidRequest, 'mediaTypes.native')) { + } else if (bidRequest.mediaTypes?.native) { let nativeRequest = buildNativeRequest(bidRequest.mediaTypes.native); imp.native = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, NATIVE_FPD), ver: '1.1', request: JSON.stringify(nativeRequest) }; - populateImpFpd(imp.native, bidRequest, NATIVE_FPD); mediaType = NATIVE; } else { throw new Error('Unsupported bid received'); @@ -315,6 +321,13 @@ function buildImp(bidRequest, secure) { return imp; } +function getDefinedParamsOrEmpty(object, params) { + if (object === undefined) { + return {}; + } + return getDefinedParams(object, params); +} + /** * Builds native request from native adunit */ @@ -344,19 +357,6 @@ function buildNativeRequest(nativeReq) { return request; } -/** - * Populate impression-level FPD from bid request - * @param target {Object} - * @param bidRequest {BidRequest} - * @param props {String[]} - */ -function populateImpFpd(target, bidRequest, props) { - if (bidRequest.ortb2Imp === undefined) { - return; - } - Object.assign(target, getDefinedParams(bidRequest.ortb2Imp, props)); -} - /** * Builds image asset request */ @@ -456,7 +456,9 @@ function makeUser(bidderRequest, fpd) { if (eids) { deepSetValue(user, 'ext.eids', eids); } - if (!isEmpty(user)) { return {user: user}; } + if (!isEmpty(user)) { + return {user: user}; + } } /** @@ -465,13 +467,17 @@ function makeUser(bidderRequest, fpd) { * @returns {{regs: Object} | undefined} */ function makeRegulations(bidderRequest) { - let {gdprConsent, uspConsent} = bidderRequest; + let {gdprConsent, uspConsent, gppConsent} = bidderRequest; let regs = {}; if (gdprConsent) { if (gdprConsent.gdprApplies !== undefined) { deepSetValue(regs, 'regs.ext.gdpr', ~~gdprConsent.gdprApplies); } } + if (gppConsent) { + deepSetValue(regs, 'regs.gpp', gppConsent.gppString); + deepSetValue(regs, 'regs.gpp_sid', gppConsent.applicableSections); + } if (uspConsent) { deepSetValue(regs, 'regs.ext.us_privacy', uspConsent); } diff --git a/modules/adlooxAnalyticsAdapter.js b/modules/adlooxAnalyticsAdapter.js index 5db75c656bb..9284d543298 100644 --- a/modules/adlooxAnalyticsAdapter.js +++ b/modules/adlooxAnalyticsAdapter.js @@ -14,7 +14,6 @@ import {find} from '../src/polyfill.js'; import {getRefererInfo} from '../src/refererDetection.js'; import { deepAccess, - getGptSlotInfoForAdUnitCode, getUniqueIdentifierStr, insertElement, isFn, @@ -28,6 +27,7 @@ import { mergeDeep, parseUrl } from '../src/utils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const MODULE = 'adlooxAnalyticsAdapter'; diff --git a/modules/adlooxRtdProvider.js b/modules/adlooxRtdProvider.js index c2037429185..727dc84e399 100644 --- a/modules/adlooxRtdProvider.js +++ b/modules/adlooxRtdProvider.js @@ -25,7 +25,6 @@ import { deepAccess, deepClone, deepSetValue, - getGptSlotInfoForAdUnitCode, isArray, isBoolean, isInteger, @@ -37,6 +36,7 @@ import { parseUrl, safeJSONParse } from '../src/utils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const MODULE_NAME = 'adloox'; const MODULE = `${MODULE_NAME}RtdProvider`; diff --git a/modules/admanBidAdapter.js b/modules/admanBidAdapter.js index ff8e6b02ad5..5eac02b7420 100644 --- a/modules/admanBidAdapter.js +++ b/modules/admanBidAdapter.js @@ -66,6 +66,7 @@ export const spec = { buildRequests: (validBidRequests = [], bidderRequest) => { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + const content = deepAccess(bidderRequest, 'ortb2.site.content', config.getAnyConfig('ortb2.site.content')); let winTop = window; let location; @@ -93,7 +94,12 @@ export const spec = { request.ccpa = bidderRequest.uspConsent; } if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent + request.gdpr = { + consentString: bidderRequest.gdprConsent.consentString + }; + } + if (content) { + request.content = content; } } const len = validBidRequests.length; @@ -109,6 +115,11 @@ export const spec = { bidFloor: getBidFloor(bid) } + if (bid.transactionId) { + placement.ext = placement.ext || {}; + placement.ext.tid = bid.transactionId; + } + if (bid.schain) { placement.schain = bid.schain; } diff --git a/modules/admaticBidAdapter.js b/modules/admaticBidAdapter.js index 027f924ac5d..fc5cf9c8f7b 100644 --- a/modules/admaticBidAdapter.js +++ b/modules/admaticBidAdapter.js @@ -1,15 +1,38 @@ -import { getValue, logError, isEmpty, deepAccess, getBidIdParameter, isArray } from '../src/utils.js'; +import {getValue, logError, isEmpty, deepAccess, isArray, getBidIdParameter} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +export const OPENRTB = { + NATIVE: { + IMAGE_TYPE: { + ICON: 1, + MAIN: 3, + }, + ASSET_ID: { + TITLE: 1, + IMAGE: 2, + ICON: 3, + BODY: 4, + SPONSORED: 5, + CTA: 6 + }, + DATA_ASSET_TYPE: { + SPONSORED: 1, + DESC: 2, + CTA_TEXT: 12, + }, + } +}; + let SYNC_URL = ''; const BIDDER_CODE = 'admatic'; + export const spec = { code: BIDDER_CODE, aliases: [ {code: 'pixad'} ], - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** f * @param {object} bid * @return {boolean} @@ -46,10 +69,10 @@ export const spec = { }, blacklist: [], site: { - page: location.href, - ref: location.origin, + page: bidderRequest.refererInfo.page, + ref: bidderRequest.refererInfo.page, publisher: { - name: location.hostname, + name: bidderRequest.refererInfo.domain, publisherId: networkId } }, @@ -106,6 +129,7 @@ export const spec = { netRevenue: true, creativeId: bid.creative_id, meta: { + model: bid.mime_type, advertiserDomains: bid && bid.adomain ? bid.adomain : [] }, bidder: bid.bidder, @@ -121,6 +145,8 @@ export const spec = { resbid.vastImpUrl = bid.iurl; } else if (resbid.mediaType === 'banner') { resbid.ad = bid.party_tag; + } else if (resbid.mediaType === 'native') { + resbid.native = interpretNativeAd(bid.party_tag) }; bidResponses.push(resbid); @@ -156,6 +182,11 @@ function enrichSlotWithFloors(slot, bidRequest) { videoSizes.forEach(videoSize => slotFloors.video[parseSize(videoSize).toString()] = bidRequest.getFloor({ size: videoSize, mediaType: VIDEO })); } + if (bidRequest.mediaTypes?.native) { + slotFloors.native = {}; + slotFloors.native['*'] = bidRequest.getFloor({ size: '*', mediaType: NATIVE }); + } + if (Object.keys(slotFloors).length > 0) { if (!slot) { slot = {} @@ -195,6 +226,16 @@ function buildRequestObject(bid) { reqObj.type = 'video'; reqObj.mediatype = bid.mediaTypes.video; } + if (bid.mediaTypes?.native) { + reqObj.type = 'native'; + reqObj.size = [{w: 1, h: 1}]; + reqObj.mediatype = bid.mediaTypes.native; + } + + if (deepAccess(bid, 'ortb2Imp.ext')) { + reqObj.ext = bid.ortb2Imp.ext; + } + reqObj.id = getBidIdParameter('bidId', bid); enrichSlotWithFloors(reqObj, bid); @@ -209,10 +250,11 @@ function getSizes(bid) { function concatSizes(bid) { let playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); let videoSizes = deepAccess(bid, 'mediaTypes.video.sizes'); + let nativeSizes = deepAccess(bid, 'mediaTypes.native.sizes'); let bannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); if (isArray(bannerSizes) || isArray(playerSize) || isArray(videoSizes)) { - let mediaTypesSizes = [bannerSizes, videoSizes, playerSize]; + let mediaTypesSizes = [bannerSizes, videoSizes, nativeSizes, playerSize]; return mediaTypesSizes .reduce(function(acc, currSize) { if (isArray(currSize)) { @@ -227,6 +269,45 @@ function concatSizes(bid) { } } +function interpretNativeAd(adm) { + const native = JSON.parse(adm).native; + const result = { + clickUrl: encodeURI(native.link.url), + impressionTrackers: native.imptrackers + }; + native.assets.forEach(asset => { + switch (asset.id) { + case OPENRTB.NATIVE.ASSET_ID.TITLE: + result.title = asset.title.text; + break; + case OPENRTB.NATIVE.ASSET_ID.IMAGE: + result.image = { + url: encodeURI(asset.img.url), + width: asset.img.w, + height: asset.img.h + }; + break; + case OPENRTB.NATIVE.ASSET_ID.ICON: + result.icon = { + url: encodeURI(asset.img.url), + width: asset.img.w, + height: asset.img.h + }; + break; + case OPENRTB.NATIVE.ASSET_ID.BODY: + result.body = asset.data.value; + break; + case OPENRTB.NATIVE.ASSET_ID.SPONSORED: + result.sponsoredBy = asset.data.value; + break; + case OPENRTB.NATIVE.ASSET_ID.CTA: + result.cta = asset.data.value; + break; + } + }); + return result; +} + function _validateId(id) { return (parseInt(id) > 0); } diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index 1006fef631c..6cbc36c1dcd 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -73,6 +73,7 @@ export const spec = { validRequest.forEach((bid) => { let imp = {}; Object.keys(bid).forEach(key => imp[key] = bid[key]); + imp.ortb2 && delete imp.ortb2; payload.imps.push(imp); }); return { diff --git a/modules/admixerIdSystem.js b/modules/admixerIdSystem.js index 7fbebecfc12..0e3a56420a8 100644 --- a/modules/admixerIdSystem.js +++ b/modules/admixerIdSystem.js @@ -72,6 +72,12 @@ export const admixerIdSubmodule = { }; return { callback: resp }; + }, + eids: { + 'admixerId': { + source: 'admixer.net', + atype: 3 + }, } }; function retrieveVisitorId(url, callback) { diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index d1192c2eb64..a2b695e55e0 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -6,16 +6,15 @@ import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'adnuntius'; const BIDDER_CODE_DEAL_ALIAS_BASE = 'adndeal'; -const BIDDER_CODE_DEAL_ALIAS_1 = BIDDER_CODE_DEAL_ALIAS_BASE + '1'; -const BIDDER_CODE_DEAL_ALIAS_2 = BIDDER_CODE_DEAL_ALIAS_BASE + '2'; -const BIDDER_CODE_DEAL_ALIAS_3 = BIDDER_CODE_DEAL_ALIAS_BASE + '3'; -const BIDDER_CODE_DEAL_ALIAS_4 = BIDDER_CODE_DEAL_ALIAS_BASE + '4'; -const BIDDER_CODE_DEAL_ALIAS_5 = BIDDER_CODE_DEAL_ALIAS_BASE + '5'; +const BIDDER_CODE_DEAL_ALIASES = [1, 2, 3, 4, 5].map(num => { + return BIDDER_CODE_DEAL_ALIAS_BASE + num; +}); const ENDPOINT_URL = 'https://ads.adnuntius.delivery/i'; const ENDPOINT_URL_EUROPE = 'https://europe.delivery.adnuntius.com/i'; const GVLID = 855; const DEFAULT_VAST_VERSION = 'vast4' const MAXIMUM_DEALS_LIMIT = 5; +const VALID_BID_TYPES = ['netBid', 'grossBid']; const checkSegment = function (segment) { if (isStr(segment)) return segment; @@ -50,17 +49,15 @@ const getUsi = function (meta, ortb2, bidderRequest) { return usi } +const validateBidType = function(bidTypeOption) { + return VALID_BID_TYPES.indexOf(bidTypeOption || '') > -1 ? bidTypeOption : 'bid'; +} + const AU_ID_REGEX = new RegExp('^[0-9A-Fa-f]{1,20}$'); export const spec = { code: BIDDER_CODE, - aliases: [ - BIDDER_CODE_DEAL_ALIAS_1, - BIDDER_CODE_DEAL_ALIAS_2, - BIDDER_CODE_DEAL_ALIAS_3, - BIDDER_CODE_DEAL_ALIAS_4, - BIDDER_CODE_DEAL_ALIAS_5 - ], + aliases: BIDDER_CODE_DEAL_ALIASES, gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function (bid) { @@ -134,6 +131,15 @@ export const spec = { interpretResponse: function (serverResponse, bidRequest) { const adUnits = serverResponse.body.adUnits; + let validatedBidType = validateBidType(config.getConfig().bidType); + if (bidRequest.bid) { + bidRequest.bid.forEach(b => { + if (b.params && b.params.bidType) { + validatedBidType = validateBidType(b.params.bidType); + } + }); + } + function buildAdResponse(bidderCode, ad, adUnit, dealCount) { const destinationUrls = ad.destinationUrls || {}; const advertiserDomains = []; @@ -143,7 +149,7 @@ export const spec = { const adResponse = { bidderCode: bidderCode, requestId: adUnit.targetId, - cpm: (ad.bid) ? ad.bid.amount * 1000 : 0, + cpm: ad[validatedBidType] ? ad[validatedBidType].amount * 1000 : 0, width: Number(ad.creativeWidth), height: Number(ad.creativeHeight), creativeId: ad.creativeId, @@ -178,7 +184,7 @@ export const spec = { const hasBidAdUnits = adUnits.filter((au) => { const bid = bidsById[au.targetId]; - if (bid && bid.bidder === BIDDER_CODE) { + if (bid && bid.bidder && BIDDER_CODE_DEAL_ALIASES.indexOf(bid.bidder) < 0) { return au.matchedAdCount > 0; } else { // We do NOT accept bids when using this adaptor via one of the diff --git a/modules/adpod.js b/modules/adpod.js index 4ab8e4e5ab9..d2fd817ee62 100644 --- a/modules/adpod.js +++ b/modules/adpod.js @@ -13,7 +13,6 @@ */ import { - compareOn, deepAccess, generateUUID, groupBy, @@ -28,7 +27,6 @@ import { import { addBidToAuction, AUCTION_IN_PROGRESS, - doCallbacksIfTimedout, getPriceByGranularity, getPriceGranularity } from '../src/auction.js'; @@ -212,9 +210,6 @@ function firePrebidCacheCall(auctionInstance, bidList, afterBidAdded) { store(bidList, function (error, cacheIds) { if (error) { logWarn(`Failed to save to the video cache: ${error}. Video bid(s) must be discarded.`); - for (let i = 0; i < bidList.length; i++) { - doCallbacksIfTimedout(auctionInstance, bidList[i]); - } } else { for (let i = 0; i < cacheIds.length; i++) { // when uuid in response is empty string then the key already existed, so this bid wasn't cached @@ -595,6 +590,23 @@ function getAdPodAdUnits(codes) { .filter((adUnit) => (codes.length > 0) ? codes.indexOf(adUnit.code) != -1 : true); } +/** + * This function will create compare function to sort on object property + * @param {string} property + * @returns {function} compare function to be used in sorting + */ +function compareOn(property) { + return function compare(a, b) { + if (a[property] < b[property]) { + return 1; + } + if (a[property] > b[property]) { + return -1; + } + return 0; + } +} + /** * This function removes bids of same category. It will be used when competitive exclusion is enabled. * @param {Array[Object]} bidsReceived diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index 8a953f0d97f..0f445fdfd78 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -1,7 +1,6 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import { logInfo, buildUrl, triggerPixel, parseSizesInput } from '../src/utils.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {buildUrl, logInfo, parseSizesInput, triggerPixel} from '../src/utils.js'; const ADQUERY_GVLID = 902; const ADQUERY_BIDDER_CODE = 'adquery'; @@ -11,7 +10,6 @@ const ADQUERY_USER_SYNC_DOMAIN = ADQUERY_BIDDER_DOMAIN_PROTOCOL + '://' + ADQUER const ADQUERY_DEFAULT_CURRENCY = 'PLN'; const ADQUERY_NET_REVENUE = true; const ADQUERY_TTL = 360; -const storage = getStorageManager({bidderCode: ADQUERY_BIDDER_CODE}); /** @type {BidderSpec} */ export const spec = { @@ -58,7 +56,6 @@ export const spec = { logInfo(request); logInfo(response); - let qid = null; const res = response && response.body && response.body.data; let bidResponses = []; @@ -87,17 +84,6 @@ export const spec = { bidResponses.push(bidResponse); logInfo('bidResponses', bidResponses); - if (res && res.qid) { - if (storage.getDataFromLocalStorage('qid')) { - qid = storage.getDataFromLocalStorage('qid'); - if (qid && qid.includes('%7B%22')) { - storage.setDataInLocalStorage('qid', res.qid); - } - } else { - storage.setDataInLocalStorage('qid', res.qid); - } - } - return bidResponses; }, @@ -130,8 +116,12 @@ export const spec = { */ onBidWon: (bid) => { logInfo('onBidWon', bid); + const bidString = JSON.stringify(bid); - const encodedBuf = window.btoa(bidString); + let copyOfBid = JSON.parse(bidString); + delete copyOfBid.ad; + const shortBidString = JSON.stringify(bid); + const encodedBuf = window.btoa(shortBidString); let params = { q: encodedBuf, @@ -187,10 +177,29 @@ export const spec = { url: syncUrl }]; } - }; + function buildRequest(validBidRequests, bidderRequest) { let bid = validBidRequests; + logInfo('buildRequest: ', bid); + + let userId = null; + if (window.qid) { + userId = window.qid; + } + + if (bid.userId && bid.userId.qid) { + userId = bid.userId.qid + } + + if (!userId) { + // onetime User ID + const ramdomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); + userId = ramdomValues.map(val => val.toString(36)).join('').substring(0, 20); + logInfo('generated onetime User ID: ', userId); + window.qid = userId; + } + let pageUrl = ''; if (bidderRequest && bidderRequest.refererInfo) { pageUrl = bidderRequest.refererInfo.page || ''; @@ -199,11 +208,10 @@ function buildRequest(validBidRequests, bidderRequest) { return { v: '$prebid.version$', placementCode: bid.params.placementId, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: bid.auctionId, + auctionId: null, type: bid.params.type, adUnitCode: bid.adUnitCode, - bidQid: storage.getDataFromLocalStorage('qid') || null, + bidQid: userId, bidId: bid.bidId, bidder: bid.bidder, bidPageUrl: pageUrl, diff --git a/modules/adqueryIdSystem.js b/modules/adqueryIdSystem.js index 5171802caba..c5d01d7fbed 100644 --- a/modules/adqueryIdSystem.js +++ b/modules/adqueryIdSystem.js @@ -8,7 +8,7 @@ import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; -import { isFn, isStr, isPlainObject, logError } from '../src/utils.js'; +import {isFn, isPlainObject, isStr, logError, logInfo} from '../src/utils.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'qid'; @@ -51,11 +51,7 @@ export const adqueryIdSubmodule = { * @returns {{qid:Object}} */ decode(value) { - let qid = storage.getDataFromLocalStorage('qid'); - if (isStr(qid)) { - return {qid: qid}; - } - return (value && typeof value['qid'] === 'string') ? { 'qid': value['qid'] } : undefined; + return {qid: value} }, /** * performs action to obtain id and return a value in the callback's response argument @@ -64,40 +60,59 @@ export const adqueryIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config) { + logInfo('adqueryIdSubmodule getId'); if (!isPlainObject(config.params)) { config.params = {}; } - const url = paramOrDefault(config.params.url, + + const url = paramOrDefault( + config.params.url, `https://bidder.adquery.io/prebid/qid`, - config.params.urlArg); + config.params.urlArg + ); const resp = function (callback) { - let qid = storage.getDataFromLocalStorage('qid'); - if (isStr(qid)) { - const responseObj = {qid: qid}; - callback(responseObj); - } else { - const callbacks = { - success: response => { - let responseObj; - if (response) { - try { - responseObj = JSON.parse(response); - } catch (error) { - logError(error); - } + let qid = window.qid; + + if (!qid) { + const ramdomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); + qid = ramdomValues.map(val => val.toString(36)).join('').substring(0, 20); + + logInfo('adqueryIdSubmodule ID QID GENERTAED:', qid); + } + logInfo('adqueryIdSubmodule ID QID:', qid); + + const callbacks = { + success: response => { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + logError(error); } - callback(responseObj); - }, - error: error => { - logError(`${MODULE_NAME}: ID fetch encountered an error`, error); - callback(); } - }; - ajax(url, callbacks, undefined, {method: 'GET'}); - } + if (responseObj.qid) { + let myQid = responseObj.qid; + storage.setDataInLocalStorage('qid', myQid); + return callback(myQid); + } + callback(); + }, + error: error => { + logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + ajax(url + '?qid=' + qid, callbacks, undefined, {method: 'GET'}); }; return {callback: resp}; + }, + eids: { + 'qid': { + source: 'adquery.io', + atype: 1 + }, } }; diff --git a/modules/adrelevantisBidAdapter.js b/modules/adrelevantisBidAdapter.js index cf785a1fc87..3c9c661b09c 100644 --- a/modules/adrelevantisBidAdapter.js +++ b/modules/adrelevantisBidAdapter.js @@ -1,8 +1,5 @@ import {Renderer} from '../src/Renderer.js'; import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, @@ -21,7 +18,10 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const BIDDER_CODE = 'adrelevantis'; const URL = 'https://ssp.adrelevantis.com/prebid'; @@ -597,6 +597,8 @@ function parseMediaType(rtbBid) { const adType = rtbBid.ad_type; if (adType === VIDEO) { return VIDEO; + } else if (adType === NATIVE) { + return NATIVE; } else { return BANNER; } diff --git a/modules/adriverBidAdapter.js b/modules/adriverBidAdapter.js index 1af0cffa700..5bce315f572 100644 --- a/modules/adriverBidAdapter.js +++ b/modules/adriverBidAdapter.js @@ -1,5 +1,5 @@ // ADRIVER BID ADAPTER for Prebid 1.13 -import { logInfo, getWindowLocation, getBidIdParameter, _each } from '../src/utils.js'; +import {logInfo, getWindowLocation, _each, getBidIdParameter} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; diff --git a/modules/adsinteractiveBidAdapter.js b/modules/adsinteractiveBidAdapter.js index 304b8bcade0..ad6bdfeb299 100644 --- a/modules/adsinteractiveBidAdapter.js +++ b/modules/adsinteractiveBidAdapter.js @@ -6,12 +6,14 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; const ADSINTERACTIVE_CODE = 'adsinteractive'; -const USER_SYNC_URL_IMAGE = 'https://pb.adsinteractive.com/img'; -const USER_SYNC_URL_IFRAME = 'https://pb.adsinteractive.com/sync'; +const USER_SYNC_URL_IMAGE = 'https://sync.adsinteractive.com/img'; +const USER_SYNC_URL_IFRAME = 'https://sync.adsinteractive.com/sync'; +const GVLID = 1212; export const spec = { code: ADSINTERACTIVE_CODE, supportedMediaTypes: [BANNER], + gvlid: GVLID, isBidRequestValid: (bid) => { return ( diff --git a/modules/adstirBidAdapter.js b/modules/adstirBidAdapter.js new file mode 100644 index 00000000000..4b22d568785 --- /dev/null +++ b/modules/adstirBidAdapter.js @@ -0,0 +1,91 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adstir'; +const ENDPOINT = 'https://ad.ad-stir.com/prebid' + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!(utils.isStr(bid.params.appId) && !utils.isEmptyStr(bid.params.appId) && utils.isInteger(bid.params.adSpaceNo)); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const sua = utils.deepAccess(validBidRequests[0], 'ortb2.device.sua', null); + + const requests = validBidRequests.map((r) => { + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify({ + appId: r.params.appId, + adSpaceNo: r.params.adSpaceNo, + auctionId: r.auctionId, + transactionId: r.transactionId, + bidId: r.bidId, + mediaTypes: r.mediaTypes, + sizes: r.sizes, + ref: { + page: bidderRequest.refererInfo.page, + tloc: bidderRequest.refererInfo.topmostLocation, + referrer: bidderRequest.refererInfo.ref, + topurl: config.getConfig('pageUrl') ? false : bidderRequest.refererInfo.reachedTop, + }, + sua, + gdpr: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies', false), + usp: (bidderRequest.uspConsent || '1---') !== '1---', + eids: utils.deepAccess(r, 'userIdAsEids', []), + schain: serializeSchain(utils.deepAccess(r, 'schain', null)), + pbVersion: '$prebid.version$', + }), + } + }); + + return requests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + const seatbid = serverResponse.body.seatbid; + if (!utils.isArray(seatbid)) { + return []; + } + const bids = []; + seatbid.forEach((b) => { + const bid = b.bid || null; + if (!bid) { + return; + } + bids.push(bid); + }); + return bids; + }, +} + +function serializeSchain(schain) { + if (!schain) { + return null; + } + + let serializedSchain = `${schain.ver},${schain.complete}`; + + schain.nodes.map(node => { + serializedSchain += `!${encodeURIComponentForRFC3986(node.asi || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.sid || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.hp || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.rid || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.name || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.domain || '')}`; + }); + + return serializedSchain; +} + +function encodeURIComponentForRFC3986(str) { + return encodeURIComponent(str).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16)}`); +} + +registerBidder(spec); diff --git a/modules/adstirBidAdapter.md b/modules/adstirBidAdapter.md new file mode 100644 index 00000000000..7485375a09d --- /dev/null +++ b/modules/adstirBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: adstir Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@ad-stir.com +``` + +# Description + +Module that connects to adstir's demand sources + +# Test Parameters + +``` + var adUnits = [ + // Banner adUnit + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'adstir', + params: { + appId: 'TEST-MEDIA', + adSpaceNo: 1, + } + } + ] + } + ]; +``` diff --git a/modules/adtargetBidAdapter.js b/modules/adtargetBidAdapter.js index 89ba4878acf..a1dec5a420f 100644 --- a/modules/adtargetBidAdapter.js +++ b/modules/adtargetBidAdapter.js @@ -1,8 +1,9 @@ -import {_map, chunk, deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; +import {_map, deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find} from '../src/polyfill.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const ENDPOINT = 'https://ghb.console.adtarget.com.tr/v2/auction/'; const BIDDER_CODE = 'adtarget'; diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index cbba1bb49eb..8e6aeecdd75 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -1,9 +1,11 @@ -import {_map, chunk, convertTypes, deepAccess, flatten, isArray, parseSizesInput} from '../src/utils.js'; +import {_map, deepAccess, flatten, isArray, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {Renderer} from '../src/Renderer.js'; import {find} from '../src/polyfill.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const subdomainSuffixes = ['', 1, 2]; const AUCTION_PATH = '/v2/auction/'; @@ -15,17 +17,12 @@ const HOST_GETTERS = { return 'ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com'; } }()), - navelix: () => 'ghb.hb.navelix.com', - appaloosa: () => 'ghb.hb.appaloosa.media', - onefiftytwomedia: () => 'ghb.ads.152media.com', - bidsxchange: () => 'ghb.hbd.bidsxchange.com', streamkey: () => 'ghb.hb.streamkey.net', janet: () => 'ghb.bidder.jmgads.com', - pgam: () => 'ghb.pgamssp.com', ocm: () => 'ghb.cenarius.orangeclickmedia.com', - vidcrunchllc: () => 'ghb.platform.vidcrunch.com', '9dotsmedia': () => 'ghb.platform.audiodots.com', - copper6: () => 'ghb.app.copper6.com' + copper6: () => 'ghb.app.copper6.com', + indicue: () => 'ghb.console.indicue.com', } const getUri = function (bidderCode) { let bidderWithoutSuffix = bidderCode.split('_')[0]; @@ -42,18 +39,13 @@ export const spec = { code: BIDDER_CODE, gvlid: 410, aliases: [ - 'onefiftytwomedia', - 'appaloosa', - 'bidsxchange', 'streamkey', 'janet', { code: 'selectmedia', gvlid: 775 }, - { code: 'navelix', gvlid: 380 }, - 'pgam', { code: 'ocm', gvlid: 1148 }, - { code: 'vidcrunchllc', gvlid: 1145 }, '9dotsmedia', 'copper6', + 'indicue', ], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { @@ -200,6 +192,14 @@ function bidToTag(bidRequests, adapterRequest) { tag.DMPId = window.adtDmp.getUID(); } + if (adapterRequest.gppConsent) { + tag.GPP = adapterRequest.gppConsent.gppString; + tag.GPPSid = adapterRequest.gppConsent.applicableSections?.toString(); + } else if (adapterRequest.ortb2?.regs?.gpp) { + tag.GPP = adapterRequest.ortb2.regs.gpp; + tag.GPPSid = adapterRequest.ortb2.regs.gpp_sid; + } + // end publisher env const bids = []; diff --git a/modules/adtelligentIdSystem.js b/modules/adtelligentIdSystem.js index fb3b5f6fe2a..440ed9ade75 100644 --- a/modules/adtelligentIdSystem.js +++ b/modules/adtelligentIdSystem.js @@ -85,6 +85,12 @@ export const adtelligentIdModule = { } } + }, + eids: { + 'adtelligentId': { + source: 'adtelligent.com', + atype: 3 + }, } }; diff --git a/modules/aduptechBidAdapter.js b/modules/aduptechBidAdapter.js index 1ea5f1a0096..49187da2fe2 100644 --- a/modules/aduptechBidAdapter.js +++ b/modules/aduptechBidAdapter.js @@ -1,7 +1,8 @@ -import {deepClone, getAdUnitSizes, isArray, isBoolean, isEmpty, isFn, isPlainObject} from '../src/utils.js'; +import {deepClone, isArray, isBoolean, isEmpty, isFn, isPlainObject} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; export const BIDDER_CODE = 'aduptech'; export const GVLID = 647; diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index 5930f3adb67..ab0ae1cbd48 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -1,307 +1,65 @@ // jshint esversion: 6, es3: false, node: true -'use strict'; - -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { - _map, - deepAccess, - deepSetValue, - getDNT, + convertTypes, isArray, - isPlainObject, - isStr, - mergeDeep, - parseSizesInput, replaceAuctionPrice, - triggerPixel + triggerPixel, + logMessage, + deepSetValue, + getBidIdParameter } from '../src/utils.js'; -import {config} from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - -const { getConfig } = config; +import { config } from '../src/config.js'; const BIDDER_CODE = 'adxcg'; const SECURE_BID_URL = 'https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'; -const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; -const NATIVE_PARAMS = { - title: { - id: 0, - name: 'title' - }, - icon: { - id: 2, - type: 1, - name: 'img' - }, - image: { - id: 3, - type: 3, - name: 'img' - }, - sponsoredBy: { - id: 5, - name: 'data', - type: 1 - }, - body: { - id: 4, - name: 'data', - type: 2 - }, - cta: { - id: 1, - type: 12, - name: 'data' - } -}; +const DEFAULT_CURRENCY = 'EUR'; +const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'battr', 'deals']; +const DEFAULT_TMAX = 500; +/** + * Adxcg Bid Adapter. + * + */ export const spec = { + code: BIDDER_CODE, - supportedMediaTypes: [ NATIVE, BANNER, VIDEO ], + + aliases: ['mediaopti'], + + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + isBidRequestValid: (bid) => { + logMessage('adxcg - validating isBidRequestValid'); const params = bid.params || {}; const { adzoneid } = params; return !!(adzoneid); }, - buildRequests: (validBidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let app, site; - - const commonFpd = bidderRequest.ortb2 || {}; - let { user } = commonFpd; - - if (typeof getConfig('app') === 'object') { - app = getConfig('app') || {}; - if (commonFpd.app) { - mergeDeep(app, commonFpd.app); - } - } else { - site = getConfig('site') || {}; - if (commonFpd.site) { - mergeDeep(site, commonFpd.site); - } - - if (!site.page) { - site.page = bidderRequest.refererInfo.page; - site.domain = bidderRequest.refererInfo.domain; - } - } - - const device = getConfig('device') || {}; - device.w = device.w || window.innerWidth; - device.h = device.h || window.innerHeight; - device.ua = device.ua || navigator.userAgent; - device.dnt = getDNT() ? 1 : 0; - device.language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - - const tid = bidderRequest.ortb2?.source?.tid; - const test = setOnAny(validBidRequests, 'params.test'); - const currency = getConfig('currency.adServerCurrency'); - const cur = currency && [ currency ]; - const eids = setOnAny(validBidRequests, 'userIdAsEids'); - const schain = setOnAny(validBidRequests, 'schain'); - - const imp = validBidRequests.map((bid, id) => { - const floorInfo = bid.getFloor ? bid.getFloor({ - currency: currency || 'USD' - }) : {}; - const bidfloor = floorInfo.floor; - const bidfloorcur = floorInfo.currency; - const { adzoneid } = bid.params; - - const imp = { - id: id + 1, - tagid: adzoneid, - secure: 1, - bidfloor, - bidfloorcur, - ext: { - } - }; - - const assets = _map(bid.nativeParams, (bidParams, key) => { - const props = NATIVE_PARAMS[key]; - const asset = { - required: bidParams.required & 1, - }; - if (props) { - asset.id = props.id; - let wmin, hmin, w, h; - let aRatios = bidParams.aspect_ratios; - - if (aRatios && aRatios[0]) { - aRatios = aRatios[0]; - wmin = aRatios.min_width || 0; - hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; - } - - if (bidParams.sizes) { - const sizes = flatten(bidParams.sizes); - w = sizes[0]; - h = sizes[1]; - } - - asset[props.name] = { - len: bidParams.len, - type: props.type, - wmin, - hmin, - w, - h - }; - - return asset; - } - }).filter(Boolean); - - if (assets.length) { - imp.native = { - request: JSON.stringify({assets: assets}) - }; - } - - const bannerParams = deepAccess(bid, 'mediaTypes.banner'); - - if (bannerParams && bannerParams.sizes) { - const sizes = parseSizesInput(bannerParams.sizes); - const format = sizes.map(size => { - const [ width, height ] = size.split('x'); - const w = parseInt(width, 10); - const h = parseInt(height, 10); - return { w, h }; - }); - - imp.banner = { - format - }; - } - - const videoParams = deepAccess(bid, 'mediaTypes.video'); - if (videoParams) { - imp.video = videoParams; - } - - return imp; - }); - - const request = { - id: bidderRequest.auctionId, - site, - app, - user, - geo: { utcoffset: new Date().getTimezoneOffset() }, - device, - source: { tid, fd: 1 }, - ext: { - prebid: { - channel: { - name: 'pbjs', - version: '$prebid.version$' - } - } - }, - cur, - imp - }; - - if (test) { - request.is_debug = !!test; - request.test = 1; - } - if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') !== undefined) { - deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1); - } - - if (bidderRequest.uspConsent) { - deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - if (eids) { - deepSetValue(request, 'user.ext.eids', eids); - } - - if (schain) { - deepSetValue(request, 'source.ext.schain', schain); - } + buildRequests: (bidRequests, bidderRequest) => { + const data = converter.toORTB({ bidRequests, bidderRequest }); return { method: 'POST', url: SECURE_BID_URL, - data: JSON.stringify(request), + data, options: { contentType: 'application/json' }, - bids: validBidRequests + bidderRequest }; }, - interpretResponse: function(serverResponse, { bids }) { - if (!serverResponse.body) { - return; - } - const { seatbid, cur } = serverResponse.body; - - const bidResponses = flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { - result[bid.impid - 1] = bid; - return result; - }, []); - - return bids.map((bid, id) => { - const bidResponse = bidResponses[id]; - if (bidResponse) { - const mediaType = deepAccess(bidResponse, 'ext.crType'); - const result = { - requestId: bid.bidId, - cpm: bidResponse.price, - creativeId: bidResponse.crid, - ttl: bidResponse.ttl ? bidResponse.ttl : 300, - netRevenue: bid.netRevenue === 'net', - currency: cur, - burl: bid.burl || '', - mediaType: mediaType, - width: bidResponse.w, - height: bidResponse.h, - dealId: bidResponse.dealid, - }; - deepSetValue(result, 'meta.mediaType', mediaType); - if (isArray(bidResponse.adomain)) { - deepSetValue(result, 'meta.advertiserDomains', bidResponse.adomain); - } - - if (isPlainObject(bidResponse.ext)) { - if (isStr(bidResponse.ext.mediaType)) { - deepSetValue(result, 'meta.mediaType', mediaType); - } - if (isStr(bidResponse.ext.advertiser_id)) { - deepSetValue(result, 'meta.advertiserId', bidResponse.ext.advertiser_id); - } - if (isStr(bidResponse.ext.advertiser_name)) { - deepSetValue(result, 'meta.advertiserName', bidResponse.ext.advertiser_name); - } - if (isStr(bidResponse.ext.agency_name)) { - deepSetValue(result, 'meta.agencyName', bidResponse.ext.agency_name); - } - } - if (mediaType === BANNER) { - result.ad = bidResponse.adm; - } else if (mediaType === NATIVE) { - result.native = parseNative(bidResponse); - result.width = 0; - result.height = 0; - } else if (mediaType === VIDEO) { - result.vastUrl = bidResponse.nurl; - result.vastXml = bidResponse.adm; - } - - return result; - } - }).filter(Boolean); + interpretResponse: (response, request) => { + if (response.body) { + const bids = converter.fromORTB({ response: response.body, request: request.data }).bids; + return bids; + } + return []; }, + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { const syncs = []; let syncUrl = config.getConfig('adxcg.usersyncUrl'); @@ -323,44 +81,95 @@ export const spec = { } return syncs; }, + onBidWon: (bid) => { // for native requests we put the nurl as an imp tracker, otherwise if the auction takes place on prebid server // the server JS adapter puts the nurl in the adm as a tracking pixel and removes the attribute if (bid.nurl) { triggerPixel(replaceAuctionPrice(bid.nurl, bid.originalCpm)) } + }, + transformBidParams: function (params) { + return convertTypes({ + 'cf': 'string', + 'cp': 'number', + 'ct': 'number', + 'adzoneid': 'string' + }, params); } }; -registerBidder(spec); +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + currency: 'EUR' + }, -function parseNative(bid) { - const { assets, link, imptrackers, jstracker } = JSON.parse(bid.adm); - const result = { - clickUrl: link.url, - clickTrackers: link.clicktrackers || undefined, - impressionTrackers: imptrackers || undefined, - javascriptTrackers: jstracker ? [ jstracker ] : undefined - }; - assets.forEach(asset => { - const kind = NATIVE_ASSET_IDS[asset.id]; - const content = kind && asset[NATIVE_PARAMS[kind].name]; - if (content) { - result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + // tagid + imp.tagid = bidRequest.params.adzoneid.toString(); + // unknown params + const unknownParams = slotUnknownParams(bidRequest); + if (imp.ext || unknownParams) { + imp.ext = Object.assign({}, imp.ext, unknownParams); + } + // battr + if (bidRequest.params.battr) { + ['banner', 'video', 'audio', 'native'].forEach(k => { + if (imp[k]) { + imp[k].battr = bidRequest.params.battr; + } + }); + } + // deals + if (bidRequest.params.deals && isArray(bidRequest.params.deals)) { + imp.pmp = { + private_auction: 0, + deals: bidRequest.params.deals + }; } - }); - return result; -} -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; + imp.secure = Number(window.location.protocol === 'https:'); + + if (!imp.bidfloor && bidRequest.params.bidFloor) { + imp.bidfloor = bidRequest.params.bidFloor; + imp.bidfloorcur = getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' } - } -} + return imp; + }, + + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + request.tmax = request.tmax || DEFAULT_TMAX; + request.test = config.getConfig('debug') ? 1 : 0; + request.at = 1; + deepSetValue(request, 'ext.prebid.channel.name', 'pbjs'); + deepSetValue(request, 'ext.prebid.channel.version', '$prebid.version$'); + return request; + }, -function flatten(arr) { - return [].concat(...arr); + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = bid.cur || DEFAULT_CURRENCY; + return bidResponse; + }, +}); + +/** + * Unknown params are captured and sent on ext + */ +function slotUnknownParams(slot) { + const ext = {}; + const knownParamsMap = {}; + KNOWN_PARAMS.forEach(value => knownParamsMap[value] = 1); + Object.keys(slot.params).forEach(key => { + if (!knownParamsMap[key]) { + ext[key] = slot.params[key]; + } + }); + return Object.keys(ext).length > 0 ? { prebid: ext } : null; } + +registerBidder(spec); diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 4080d9f25cd..8952c3ae2b9 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,5 +1,6 @@ import {buildUrl, deepAccess, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; import {find} from '../src/polyfill.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; @@ -63,6 +64,7 @@ export const spec = { // convert Native ORTB definition to old-style prebid native definition bidRequests = convertOrtbRequestToProprietaryNative(bidRequests); let hasVideo = false; + let eids; const payload = { Version: VERSION, Bids: bidRequests.reduce((accumulator, bidReq) => { @@ -81,6 +83,9 @@ export const spec = { if (bidReq.schain) { accumulator[bidReq.bidId].SChain = bidReq.schain; } + if (!eids && bidReq.userIdAsEids && bidReq.userIdAsEids.length) { + eids = bidReq.userIdAsEids; + } if (mediatype === NATIVE) { let nativeReq = bidReq.mediaTypes.native; if (nativeReq.type === 'image') { @@ -120,9 +125,8 @@ export const spec = { if (bidderRequest.ortb2) { payload.ortb2 = bidderRequest.ortb2; } - - if (deepAccess(bidderRequest, 'userIdAsEids')) { - payload.userId = bidderRequest.userIdAsEids; + if (eids) { + payload.eids = eids; } payload.pbjs_version = '$prebid.version$'; @@ -163,6 +167,50 @@ export const spec = { } }); return bidResponses; + }, + + /** + * List user sync endpoints. + * Legal information have to be added to the request. + * Only iframe syncs are supported. + * + * @param {*} syncOptions Publisher prebid configuration. + * @param {*} serverResponses A successful response from the server. + * @return {syncs[]} An array of syncs that should be executed. + */ + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + if (!syncOptions.iframeEnabled) { + return []; + } + + let params = ''; + + // GDPR + if (gdprConsent) { + params += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + params += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + + // coppa compliance + if (config.getConfig('coppa') === true) { + params += '&coppa=1'; + } + + // CCPA + if (uspConsent) { + params += '&us_privacy=' + encodeURIComponent(uspConsent); + } + + // GPP + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppConsent.gppString); + params += '&gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(',')); + } + + return [{ + type: 'iframe', + url: `https://visitor.omnitagjs.com/visitor/isync?uid=19340f4f097d16f41f34fc0274981ca4${params}` + }]; } } diff --git a/modules/agmaAnalyticsAdapter.js b/modules/agmaAnalyticsAdapter.js new file mode 100644 index 00000000000..afbc3e771ec --- /dev/null +++ b/modules/agmaAnalyticsAdapter.js @@ -0,0 +1,225 @@ +import { ajax } from '../src/ajax.js'; +import { + generateUUID, + logInfo, + logError, + getPerformanceNow, + isEmpty, + isEmptyStr, +} from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager, { gdprDataHandler } from '../src/adapterManager.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { config } from '../src/config.js'; + +const GVLID = 1122; +const ModuleCode = 'agma'; +const analyticsType = 'endpoint'; +const scriptVersion = '1.7.0'; +const batchDelayInMs = 1000; +const agmaURL = 'https://pbc.agma-analytics.de/v1'; +const pageViewId = generateUUID(); + +const { + EVENTS: { AUCTION_INIT }, +} = CONSTANTS; + +// Helper functions +const getScreen = () => { + const w = window; + const d = document; + const e = d.documentElement; + const g = d.getElementsByTagName('body')[0]; + const x = w.innerWidth || e.clientWidth || g.clientWidth; + const y = w.innerHeight || e.clientHeight || g.clientHeight; + return { x, y }; +}; + +const getUserIDs = () => { + try { + return getGlobal().getUserIdsAsEids(); + } catch (e) {} + return []; +}; + +export const getOrtb2Data = (options) => { + let site = null; + let user = null; + + // check if data is provided via config + if (options.ortb2) { + if (options.ortb2.user) { + user = options.ortb2.user; + } + if (options.ortb2.site) { + site = options.ortb2.site; + } + if (site && user) { + return { site, user }; + } + } + try { + const configData = config.getConfig('agma'); + // try to fallback to global config + if (configData.ortb2) { + site = site || configData.ortb2.site; + user = user || configData.ortb2.user; + } + } catch (e) {} + + return { site, user }; +}; + +export const getTiming = () => { + // Timing API V2 + let ttfb = 0; + try { + const entry = performance.getEntriesByType('navigation')[0]; + ttfb = Math.round(entry.responseStart - entry.startTime); + } catch (e) { + // Timing API V1 + try { + const entry = performance.timing; + ttfb = Math.round(entry.responseStart - entry.fetchStart); + } catch (e) { + // Timing API not available + return null; + } + } + const elapsedTime = getPerformanceNow(); + ttfb = ttfb >= 0 && ttfb <= elapsedTime ? ttfb : 0; + return { + ttfb, + elapsedTime, + }; +}; + +export const getPayload = (auctionIds, options) => { + if (!options || !auctionIds || auctionIds.length === 0) { + return false; + } + const consentData = gdprDataHandler.getConsentData(); + let gdprApplies = true; // we assume gdpr applies + let useExtendedPayload = false; + if (consentData) { + gdprApplies = consentData.gdprApplies; + const consents = consentData.vendorData?.vendor?.consents || {}; + useExtendedPayload = consents[GVLID]; + } + const ortb2 = getOrtb2Data(options); + const ri = getRefererInfo() || {}; + + let payload = { + auctionIds: auctionIds, + triggerEvent: options.triggerEvent, + pageViewId, + domain: ri.domain, + gdprApplies, + code: options.code, + ortb2: { site: ortb2.site }, + pageUrl: ri.page, + prebidVersion: '$prebid.version$', + scriptVersion, + debug: options.debug, + timing: getTiming(), + }; + + if (useExtendedPayload) { + const { x, y } = getScreen(); + const userIdsAsEids = getUserIDs(); + payload = { + ...payload, + ortb2, + extended: true, + timestamp: Date.now(), + gdprConsentString: consentData.consentString, + timezoneOffset: new Date().getTimezoneOffset(), + language: window.navigator.language, + referrer: ri.topmostLocation, + pageUrl: ri.page, + screenWidth: x, + screenHeight: y, + userIdsAsEids, + }; + } + return payload; +}; + +const agmaAnalytics = Object.assign(adapter({ analyticsType }), { + auctionIds: [], + timer: null, + track(data) { + const { eventType, args } = data; + if (eventType === this.options.triggerEvent && args && args.auctionId) { + this.auctionIds.push(args.auctionId); + if (this.timer === null) { + this.timer = setTimeout(() => { + this.processBatch(); + }, batchDelayInMs); + } + } + }, + processBatch() { + const currentBatch = [...this.auctionIds]; + const payload = getPayload(currentBatch, this.options); + this.auctionIds = []; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + this.send(payload); + }, + send(payload) { + if (!payload) { + return; + } + return ajax( + agmaURL, + () => { + logInfo(ModuleCode, 'flushed', payload); + }, + JSON.stringify(payload), + { + contentType: 'text/plain', + method: 'POST', + } + ); + }, +}); + +agmaAnalytics.originEnableAnalytics = agmaAnalytics.enableAnalytics; +agmaAnalytics.enableAnalytics = function (config = {}) { + const { options } = config; + + if (isEmpty(options)) { + logError(ModuleCode, 'Please set options'); + return false; + } + + if (options.site && !options.code) { + logError(ModuleCode, 'Please set `code` - `site` is deprecated'); + options.code = options.site; + } + + if (!options.code || isEmptyStr(options.code)) { + logError(ModuleCode, 'Please set `code` option - agma Analytics is disabled'); + return false; + } + + agmaAnalytics.options = { + triggerEvent: AUCTION_INIT, + ...options, + }; + + agmaAnalytics.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: agmaAnalytics, + code: ModuleCode, + gvlid: GVLID, +}); + +export default agmaAnalytics; diff --git a/modules/agmaAnalyticsAdapter.md b/modules/agmaAnalyticsAdapter.md new file mode 100644 index 00000000000..30c88fb92ec --- /dev/null +++ b/modules/agmaAnalyticsAdapter.md @@ -0,0 +1,28 @@ +# Overview + Module Name: Agma Analytics + Module Type: Analytics Adapter + Maintainer: [www.agma-mmc.de](https://www.agma-mmc.de) + Technical Support: [info@mllrsohn.com](mailto:info@mllrsohn.com) + +# Description + +Agma Analytics adapter. Please contact [team-internet@agma-mmc.de](mailto:team-internet@agma-mmc.de) for signup and access to [futher documentation](https://docs.agma-analytics.de). + +# Usage + +Add the `agmaAnalyticsAdapter` to your build: + +``` +gulp build --modules=...,agmaAnalyticsAdapter... +``` + +Configure the analytics module: + +```javascript +pbjs.enableAnalytics({ + provider: 'agma', + options: { + code: 'provided-by-agma' // change to the code you received from agma + } +}); +``` diff --git a/modules/aidemBidAdapter.js b/modules/aidemBidAdapter.js index 7469f26156b..c6a5cd96fb6 100644 --- a/modules/aidemBidAdapter.js +++ b/modules/aidemBidAdapter.js @@ -1,27 +1,15 @@ -import { - _each, - contains, - deepAccess, - deepSetValue, - getDNT, - isBoolean, - isNumber, - isStr, - logError, - logInfo -} from '../src/utils.js'; +import {deepAccess, deepSetValue, isBoolean, isNumber, isStr, logError, logInfo} from '../src/utils.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {ajax} from '../src/ajax.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'aidem'; const BASE_URL = 'https://zero.aidemsrv.com'; const LOCAL_BASE_URL = 'http://127.0.0.1:8787'; -const AVAILABLE_CURRENCIES = ['USD']; -const DEFAULT_CURRENCY = ['USD']; // NOTE - USD is the only supported currency right now; Hardcoded for bids const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; const REQUIRED_VIDEO_PARAMS = [ 'mimes', 'protocols', 'context' ]; @@ -37,34 +25,63 @@ export const ERROR_CODES = { }; const endpoints = { - request: `${BASE_URL}/bid/request`, - notice: { - win: `${BASE_URL}/notice/win`, - timeout: `${BASE_URL}/notice/timeout`, - error: `${BASE_URL}/notice/error`, - } + request: `${BASE_URL}/prebidjs/ortb/v2.6/bid/request`, + // notice: { + // win: `${BASE_URL}/notice/win`, + // timeout: `${BASE_URL}/notice/timeout`, + // error: `${BASE_URL}/notice/error`, + // } }; -export function setEndPoints(env = null, path = '', mediaType = BANNER) { +export function setEndPoints(env = null, path = '') { switch (env) { case 'local': - endpoints.request = mediaType === BANNER ? `${LOCAL_BASE_URL}${path}/bid/request` : `${LOCAL_BASE_URL}${path}/bid/videorequest`; - endpoints.notice.win = `${LOCAL_BASE_URL}${path}/notice/win`; - endpoints.notice.error = `${LOCAL_BASE_URL}${path}/notice/error`; - endpoints.notice.timeout = `${LOCAL_BASE_URL}${path}/notice/timeout`; + endpoints.request = `${LOCAL_BASE_URL}${path}/prebidjs/ortb/v2.6/bid/request`; break; case 'main': - endpoints.request = mediaType === BANNER ? `${BASE_URL}${path}/bid/request` : `${BASE_URL}${path}/bid/videorequest`; - endpoints.notice.win = `${BASE_URL}${path}/notice/win`; - endpoints.notice.error = `${BASE_URL}${path}/notice/error`; - endpoints.notice.timeout = `${BASE_URL}${path}/notice/timeout`; + endpoints.request = `${BASE_URL}${path}/prebidjs/ortb/v2.6/bid/request`; break; } return endpoints; } config.getConfig('aidem', function (config) { - if (config.aidem.env) { setEndPoints(config.aidem.env, config.aidem.path, config.aidem.mediaType); } + if (config.aidem.env) { setEndPoints(config.aidem.env, config.aidem.path); } +}); + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + }, + request(buildRequest, imps, bidderRequest, context) { + logInfo('Building request'); + const request = buildRequest(imps, bidderRequest, context); + deepSetValue(request, 'at', 1); + setPrebidRequestEnvironment(request); + deepSetValue(request, 'regs', getRegs()); + deepSetValue(request, 'site.publisher.id', bidderRequest.bids[0].params.publisherId); + deepSetValue(request, 'site.id', bidderRequest.bids[0].params.siteId); + return request; + }, + imp(buildImp, bidRequest, context) { + logInfo('Building imp bidRequest', bidRequest); + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'tagId', bidRequest.params.placementId); + return imp; + }, + bidResponse(buildBidResponse, bid, context) { + const {bidRequest} = context; + const bidResponse = buildBidResponse(bid, context); + logInfo('Building bidResponse'); + logInfo('bid', bid); + logInfo('bidRequest', bidRequest); + logInfo('bidResponse', bidResponse); + if (bidResponse.mediaType === VIDEO) { + deepSetValue(bidResponse, 'vastUrl', bid.adm); + } + return bidResponse; + } }); // AIDEM Custom FN @@ -89,49 +106,6 @@ function recur(obj) { return result; } -// ================================================================================= -function getConnectionType() { - const connection = navigator.connection || navigator.webkitConnection; - if (!connection) { - return 0; - } - switch (connection.type) { - case 'ethernet': - return 1; - case 'wifi': - return 2; - case 'cellular': - switch (connection.effectiveType) { - case 'slow-2g': - return 4; - case '2g': - return 4; - case '3g': - return 5; - case '4g': - return 6; - case '5g': - return 7; - default: - return 3; - } - default: - return 0; - } -} - -function getDevice() { - const language = navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage; - return { - ua: navigator.userAgent, - dnt: !!getDNT(), - language: language, - connectiontype: getConnectionType(), - screen_width: screen.width, - screen_height: screen.height - }; -} - function getRegs() { let regs = {}; const consentManagement = config.getConfig('consentManagement'); @@ -157,129 +131,6 @@ function getRegs() { return regs; } -function getPageUrl(bidderRequest) { - return bidderRequest?.refererInfo?.page; -} - -function buildWinNotice(bid) { - const params = bid.params[0]; - const app = deepAccess(bid, 'meta.ext.app') - const winNoticeExt = deepAccess(bid, 'meta.ext.win_notice_ext') - return { - publisherId: params.publisherId, - siteId: params.siteId, - placementId: params.placementId, - burl: deepAccess(bid, 'meta.burl'), - cpm: bid.cpm, - currency: bid.currency, - impid: deepAccess(bid, 'meta.impid'), - dsp_id: deepAccess(bid, 'meta.dsp_id'), - adUnitCode: bid.adUnitCode, - // TODO: fix auctionId/transactionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: bid.auctionId, - transactionId: bid.transactionId, - ttl: bid.ttl, - requestTimestamp: bid.requestTimestamp, - responseTimestamp: bid.responseTimestamp, - mediatype: bid.mediaType, - environment: app ? 'app' : 'web', - ...app, - ext: winNoticeExt, - }; -} - -function buildErrorNotice(prebidErrorResponse) { - return { - message: `Prebid.js: Server call for ${prebidErrorResponse.bidderCode} failed.`, - url: encodeURIComponent(getPageUrl(prebidErrorResponse)), - auctionId: prebidErrorResponse.auctionId, - bidderRequestId: prebidErrorResponse.bidderRequestId, - metrics: {} - }; -} - -function hasValidFloor(obj) { - if (!obj) return false; - const hasValue = !isNaN(Number(obj.value)); - const hasCurrency = contains(AVAILABLE_CURRENCIES, obj.currency); - return hasValue && hasCurrency; -} - -function getMediaType(bidRequest) { - if ((bidRequest.mediaTypes && bidRequest.mediaTypes.hasOwnProperty('video')) || bidRequest.params.hasOwnProperty('video')) { return VIDEO; } - return BANNER; -} - -function getPrebidRequestFields(bidderRequest, bidRequests) { - const payload = {}; - // Base Payload Data - deepSetValue(payload, 'id', bidderRequest.bidderRequestId); - // Impressions - setPrebidImpressionObject(bidRequests, payload); - // Device - deepSetValue(payload, 'device', getDevice()); - // Timeout - deepSetValue(payload, 'tmax', bidderRequest.timeout); - // Currency - deepSetValue(payload, 'cur', DEFAULT_CURRENCY); - // Timezone - deepSetValue(payload, 'tz', new Date().getTimezoneOffset()); - // Privacy Regs - deepSetValue(payload, 'regs', getRegs()); - // Site - setPrebidSiteObject(bidderRequest, payload); - // Environment - setPrebidRequestEnvironment(payload); - // AT auction type - deepSetValue(payload, 'at', 1); - - return payload; -} - -function setPrebidImpressionObject(bidRequests, payload) { - payload.imp = []; - _each(bidRequests, function (bidRequest) { - const impressionObject = {}; - // Placement or ad tag used to initiate the auction - deepSetValue(impressionObject, 'id', bidRequest.bidId); - // Transaction id - // TODO: `imp.tid` is not ORTB, is this intentional? - deepSetValue(impressionObject, 'tid', deepAccess(bidRequest, 'ortb2Imp.ext.tid')); - // placement id - deepSetValue(impressionObject, 'tagid', deepAccess(bidRequest, 'params.placementId', null)); - // Publisher id - deepSetValue(payload, 'site.publisher.id', deepAccess(bidRequest, 'params.publisherId')); - // Site id - deepSetValue(payload, 'site.id', deepAccess(bidRequest, 'params.siteId')); - const mediaType = getMediaType(bidRequest); - switch (mediaType) { - case 'banner': - setPrebidImpressionObjectBanner(bidRequest, impressionObject); - break; - case 'video': - setPrebidImpressionObjectVideo(bidRequest, impressionObject); - break; - } - - // Floor (optional) - setPrebidImpressionObjectFloor(bidRequest, impressionObject); - - impressionObject.imp_ext = {}; - - payload.imp.push(impressionObject); - }); -} - -function setPrebidSiteObject(bidderRequest, payload) { - deepSetValue(payload, 'site.domain', deepAccess(bidderRequest, 'refererInfo.domain')); - deepSetValue(payload, 'site.page', deepAccess(bidderRequest, 'refererInfo.page')); - deepSetValue(payload, 'site.referer', deepAccess(bidderRequest, 'refererInfo.ref')); - deepSetValue(payload, 'site.cat', deepAccess(bidderRequest, 'ortb2.site.cat')); - deepSetValue(payload, 'site.sectioncat', deepAccess(bidderRequest, 'ortb2.site.sectioncat')); - deepSetValue(payload, 'site.keywords', deepAccess(bidderRequest, 'ortb2.site.keywords')); - deepSetValue(payload, 'site.site_ext', deepAccess(bidderRequest, 'ortb2.site.ext')); // see https://docs.prebid.org/features/firstPartyData.html -} - function setPrebidRequestEnvironment(payload) { const __navigator = JSON.parse(JSON.stringify(recur(navigator))); delete __navigator.plugins; @@ -296,92 +147,6 @@ function setPrebidRequestEnvironment(payload) { deepSetValue(payload, 'environment.wpar.innerHeight', window.innerHeight); } -function setPrebidImpressionObjectFloor(bidRequest, impressionObject) { - const floor = deepAccess(bidRequest, 'params.floor'); - if (hasValidFloor(floor)) { - deepSetValue(impressionObject, 'floor.value', floor.value); - deepSetValue(impressionObject, 'floor.currency', floor.currency); - } -} - -function setPrebidImpressionObjectBanner(bidRequest, impressionObject) { - deepSetValue(impressionObject, 'mediatype', BANNER); - deepSetValue(impressionObject, 'banner.topframe', 1); - deepSetValue(impressionObject, 'banner.format', []); - _each(bidRequest.mediaTypes.banner.sizes, function (bannerFormat) { - const format = {}; - deepSetValue(format, 'w', bannerFormat[0]); - deepSetValue(format, 'h', bannerFormat[1]); - deepSetValue(format, 'format_ext', {}); - impressionObject.banner.format.push(format); - }); -} - -function setPrebidImpressionObjectVideo(bidRequest, impressionObject) { - deepSetValue(impressionObject, 'mediatype', VIDEO); - deepSetValue(impressionObject, 'video.format', []); - deepSetValue(impressionObject, 'video.mimes', bidRequest.mediaTypes.video.mimes); - deepSetValue(impressionObject, 'video.minDuration', bidRequest.mediaTypes.video.minduration); - deepSetValue(impressionObject, 'video.maxDuration', bidRequest.mediaTypes.video.maxduration); - deepSetValue(impressionObject, 'video.protocols', bidRequest.mediaTypes.video.protocols); - deepSetValue(impressionObject, 'video.context', bidRequest.mediaTypes.video.context); - deepSetValue(impressionObject, 'video.playbackmethod', bidRequest.mediaTypes.video.playbackmethod); - deepSetValue(impressionObject, 'skip', bidRequest.mediaTypes.video.skip); - deepSetValue(impressionObject, 'skipafter', bidRequest.mediaTypes.video.skipafter); - deepSetValue(impressionObject, 'video.pos', bidRequest.mediaTypes.video.pos); - _each(bidRequest.mediaTypes.video.playerSize, function (videoPlayerSize) { - const format = {}; - deepSetValue(format, 'w', videoPlayerSize[0]); - deepSetValue(format, 'h', videoPlayerSize[1]); - deepSetValue(format, 'format_ext', {}); - impressionObject.video.format.push(format); - }); -} - -function getPrebidResponseBidObject(openRTBResponseBidObject) { - const prebidResponseBidObject = {}; - // Common properties - deepSetValue(prebidResponseBidObject, 'requestId', openRTBResponseBidObject.impid); - deepSetValue(prebidResponseBidObject, 'cpm', parseFloat(openRTBResponseBidObject.price)); - deepSetValue(prebidResponseBidObject, 'creativeId', openRTBResponseBidObject.crid); - deepSetValue(prebidResponseBidObject, 'currency', openRTBResponseBidObject.cur ? openRTBResponseBidObject.cur.toUpperCase() : DEFAULT_CURRENCY); - deepSetValue(prebidResponseBidObject, 'width', openRTBResponseBidObject.w); - deepSetValue(prebidResponseBidObject, 'height', openRTBResponseBidObject.h); - deepSetValue(prebidResponseBidObject, 'dealId', openRTBResponseBidObject.dealid); - deepSetValue(prebidResponseBidObject, 'netRevenue', true); - deepSetValue(prebidResponseBidObject, 'ttl', 60000); - - if (openRTBResponseBidObject.mediatype === VIDEO) { - logInfo('bidObject.mediatype == VIDEO'); - deepSetValue(prebidResponseBidObject, 'mediaType', VIDEO); - deepSetValue(prebidResponseBidObject, 'vastUrl', openRTBResponseBidObject.adm); - } else { - logInfo('bidObject.mediatype == BANNER'); - deepSetValue(prebidResponseBidObject, 'mediaType', BANNER); - deepSetValue(prebidResponseBidObject, 'ad', openRTBResponseBidObject.adm); - } - setPrebidResponseBidObjectMeta(prebidResponseBidObject, openRTBResponseBidObject); - return prebidResponseBidObject; -} - -function setPrebidResponseBidObjectMeta(prebidResponseBidObject, openRTBResponseBidObject) { - logInfo('AIDEM Bid Adapter meta', openRTBResponseBidObject); - deepSetValue(prebidResponseBidObject, 'meta.advertiserDomains', deepAccess(openRTBResponseBidObject, 'meta.advertiserDomains')); - deepSetValue(prebidResponseBidObject, 'meta.ext', deepAccess(openRTBResponseBidObject, 'meta.ext')); - if (openRTBResponseBidObject.cat && Array.isArray(openRTBResponseBidObject.cat)) { - const primaryCatId = openRTBResponseBidObject.cat.shift(); - deepSetValue(prebidResponseBidObject, 'meta.primaryCatId', primaryCatId); - deepSetValue(prebidResponseBidObject, 'meta.secondaryCatIds', openRTBResponseBidObject.cat); - } - deepSetValue(prebidResponseBidObject, 'meta.id', openRTBResponseBidObject.id); - deepSetValue(prebidResponseBidObject, 'meta.dsp_id', openRTBResponseBidObject.dsp_id); - deepSetValue(prebidResponseBidObject, 'meta.adid', openRTBResponseBidObject.adid); - deepSetValue(prebidResponseBidObject, 'meta.burl', openRTBResponseBidObject.burl); - deepSetValue(prebidResponseBidObject, 'meta.impid', openRTBResponseBidObject.impid); - deepSetValue(prebidResponseBidObject, 'meta.cat', openRTBResponseBidObject.cat); - deepSetValue(prebidResponseBidObject, 'meta.cid', openRTBResponseBidObject.cid); -} - function hasValidMediaType(bidRequest) { const supported = hasBannerMediaType(bidRequest) || hasVideoMediaType(bidRequest); if (!supported) { @@ -494,48 +259,38 @@ export const spec = { return passesRateLimit(bidRequest); }, - buildRequests: function(validBidRequests, bidderRequest) { - logInfo('validBidRequests: ', validBidRequests); + buildRequests: function(bidRequests, bidderRequest) { + logInfo('bidRequests: ', bidRequests); logInfo('bidderRequest: ', bidderRequest); - const prebidRequest = getPrebidRequestFields(bidderRequest, validBidRequests); - const payloadString = JSON.stringify(prebidRequest); - + const data = converter.toORTB({bidRequests, bidderRequest}); + logInfo('request payload', data); return { method: 'POST', url: endpoints.request, - data: payloadString, + data, options: { withCredentials: true } }; }, - interpretResponse: function (serverResponse) { - const bids = []; - logInfo('serverResponse: ', serverResponse); - _each(serverResponse.body.bid, function (bidObject) { - logInfo('bidObject: ', bidObject); - if (!bidObject.price || !bidObject.adm) { - return; - } - logInfo('CPM OK'); - const bid = getPrebidResponseBidObject(bidObject); - bids.push(bid); - }); - return bids; + interpretResponse: function (serverResponse, request) { + logInfo('serverResponse body: ', serverResponse.body); + logInfo('request data: ', request.data); + const ortbBids = converter.fromORTB({response: serverResponse.body, request: request.data}).bids; + logInfo('ortbBids: ', ortbBids); + return ortbBids; }, onBidWon: function(bid) { // Bidder specific code logInfo('onBidWon bid: ', bid); - const notice = buildWinNotice(bid); - ajax(endpoints.notice.win, null, JSON.stringify(notice), { method: 'POST', withCredentials: true }); + ajax(bid.burl); }, - onBidderError: function({ bidderRequest }) { - // Bidder specific code - const notice = buildErrorNotice(bidderRequest); - ajax(endpoints.notice.error, null, JSON.stringify(notice), { method: 'POST', withCredentials: true }); - }, + // onBidderError: function({ bidderRequest }) { + // const notice = buildErrorNotice(bidderRequest); + // ajax(endpoints.notice.error, null, JSON.stringify(notice), { method: 'POST', withCredentials: true }); + // }, }; registerBidder(spec); diff --git a/modules/ajaBidAdapter.js b/modules/ajaBidAdapter.js index ffab41611ef..9049197e565 100644 --- a/modules/ajaBidAdapter.js +++ b/modules/ajaBidAdapter.js @@ -1,7 +1,8 @@ -import { getBidIdParameter, tryAppendQueryString, createTrackPixelHtml, logError, logWarn, deepAccess } from '../src/utils.js'; +import {createTrackPixelHtml, logError, logWarn, deepAccess, getBidIdParameter} from '../src/utils.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO, BANNER, NATIVE } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BidderCode = 'aja'; const URL = 'https://ad.as.amanad.adtdp.com/v2/prebid'; diff --git a/modules/alkimiBidAdapter.js b/modules/alkimiBidAdapter.js index c087b3061a0..53f6460434f 100644 --- a/modules/alkimiBidAdapter.js +++ b/modules/alkimiBidAdapter.js @@ -1,18 +1,20 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {deepAccess, deepClone} from '../src/utils.js'; +import {deepAccess, deepClone, getDNT, generateUUID, replaceAuctionPrice} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; -import {VIDEO} from '../src/mediaTypes.js'; +import {VIDEO, BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; const BIDDER_CODE = 'alkimi'; +const GVLID = 1169; export const ENDPOINT = 'https://exchange.alkimi-onboarding.com/bid?prebid=true'; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { - return !!(bid.params && bid.params.bidFloor && bid.params.token); + return !!(bid.params && bid.params.token); }, buildRequests: function (validBidRequests, bidderRequest) { @@ -28,12 +30,15 @@ export const spec = { bids.push({ token: bidRequest.params.token, - pos: bidRequest.params.pos, + instl: bidRequest.params.instl, + exp: bidRequest.params.exp, bidFloor: getBidFloor(bidRequest, formatTypes), sizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes')), playerSizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize')), impMediaTypes: formatTypes, - adUnitCode: bidRequest.adUnitCode + adUnitCode: bidRequest.adUnitCode, + video: deepAccess(bidRequest, 'mediaTypes.video'), + banner: deepAccess(bidRequest, 'mediaTypes.banner') }) bidIds.push(bidRequest.bidId) }) @@ -41,14 +46,26 @@ export const spec = { const alkimiConfig = config.getConfig('alkimi'); let payload = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - requestId: bidderRequest.auctionId, + requestId: generateUUID(), signRequest: {bids, randomUUID: alkimiConfig && alkimiConfig.randomUUID}, bidIds, referer: bidderRequest.refererInfo.page, signature: alkimiConfig && alkimiConfig.signature, schain: validBidRequests[0].schain, - cpp: config.getConfig('coppa') ? 1 : 0 + cpp: config.getConfig('coppa') ? 1 : 0, + device: { + dnt: getDNT() ? 1 : 0, + w: screen.width, + h: screen.height + }, + ortb2: { + site: { + keywords: bidderRequest.ortb2?.site?.keywords + }, + at: bidderRequest.ortb2?.at, + bcat: bidderRequest.ortb2?.bcat, + wseat: bidderRequest.ortb2?.wseat + } } if (bidderRequest && bidderRequest.gdprConsent) { @@ -99,7 +116,7 @@ export const spec = { // banner or video if (VIDEO === bid.mediaType) { - bid.vastXml = bid.ad; + bid.vastUrl = replaceAuctionPrice(bid.winUrl, bid.cpm); } bid.meta = {}; @@ -112,21 +129,12 @@ export const spec = { }, onBidWon: function (bid) { - let winUrl; - if (bid.winUrl || bid.vastUrl) { - winUrl = bid.winUrl ? bid.winUrl : bid.vastUrl; - winUrl = winUrl.replace(/\$\{AUCTION_PRICE}/, bid.cpm); - } else if (bid.ad) { - let trackImg = bid.ad.match(/(?!^)/); - bid.ad = bid.ad.replace(trackImg[0], ''); - winUrl = trackImg[0].split('"')[1]; - winUrl = winUrl.replace(/\$%7BAUCTION_PRICE%7D/, bid.cpm); - } else { - return false; + if (BANNER == bid.mediaType && bid.winUrl) { + const winUrl = replaceAuctionPrice(bid.winUrl, bid.cpm); + ajax(winUrl, null); + return true; } - - ajax(winUrl, null); - return true; + return false; } } diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index 7286001b888..a773ac70559 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -1,5 +1,5 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { _each, deepAccess, @@ -10,15 +10,16 @@ import { logError, parseUrl, triggerPixel, + generateUUID, } from '../src/utils.js'; -import {config} from '../src/config.js'; -import {getStorageManager} from '../src/storageManager.js'; +import { config } from '../src/config.js'; +import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'amx'; const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const SIMPLE_TLD_TEST = /\.com?\.\w{2,4}$/; const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; -const VERSION = 'pba1.3.2'; +const VERSION = 'pba1.3.3'; const VAST_RXP = /^\s*<\??(?:vast|xml)/i; const TRACKING_ENDPOINT = 'https://1x1.a-mo.net/hbx/'; const AMUID_KEY = '__amuidpb'; @@ -255,12 +256,12 @@ function getGpp(bidderRequest) { return bidderRequest.gppConsent; } - return bidderRequest?.ortb2?.regs?.gpp ?? {gppString: '', applicableSections: ''}; + return bidderRequest?.ortb2?.regs?.gpp ?? { gppString: '', applicableSections: '' }; } function buildReferrerInfo(bidderRequest) { if (bidderRequest.refererInfo == null) { - return {r: '', t: false, c: '', l: 0, s: []} + return { r: '', t: false, c: '', l: 0, s: [] } } const re = bidderRequest.refererInfo; @@ -303,8 +304,7 @@ export const spec = { }; const payload = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - a: bidderRequest.auctionId, + a: generateUUID(), B: 0, b: loc.host, brc: fbid.bidderRequestsCount || 0, @@ -388,9 +388,9 @@ export const spec = { const output = []; let hasFrame = false; - _each(serverResponses, function ({ body: response }) { + _each(serverResponses, function({ body: response }) { if (response != null && response.p != null && response.p.hreq) { - _each(response.p.hreq, function (syncPixel) { + _each(response.p.hreq, function(syncPixel) { const pixelType = syncPixel.indexOf('__st=iframe') !== -1 ? 'iframe' : 'image'; if (syncOptions.iframeEnabled || pixelType === 'image') { @@ -483,7 +483,6 @@ export const spec = { bid: timeoutData.bidId, a: timeoutData.adUnitCode, cn: timeoutData.timeout, - aud: timeoutData.auctionId, }); }, diff --git a/modules/amxIdSystem.js b/modules/amxIdSystem.js index 5deffa00dfe..184eff76c34 100644 --- a/modules/amxIdSystem.js +++ b/modules/amxIdSystem.js @@ -152,6 +152,12 @@ export const amxIdSubmodule = { return { callback }; }, + eids: { + amxId: { + source: 'amxdt.net', + atype: 1, + }, + } }; submodule('userId', amxIdSubmodule); diff --git a/modules/apacdexBidAdapter.js b/modules/apacdexBidAdapter.js index e1557d9c6d3..834df134c2e 100644 --- a/modules/apacdexBidAdapter.js +++ b/modules/apacdexBidAdapter.js @@ -96,8 +96,8 @@ export const spec = { payload.device = {}; payload.device.ua = navigator.userAgent; - payload.device.height = window.screen.width; - payload.device.width = window.screen.height; + payload.device.height = window.screen.height; + payload.device.width = window.screen.width; payload.device.dnt = _getDoNotTrack(); payload.device.language = navigator.language; diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 0660f4f4b10..c6230d9f1e4 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -1,17 +1,10 @@ import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, getUniqueIdentifierStr, - getWindowFromDocument, isArray, isArrayOfNums, isEmpty, @@ -40,7 +33,10 @@ import { getANKewyordParamFromMaps, getANKeywordParam, transformBidderParamKeywords -} from '../libraries/appnexusKeywords/anKeywords.js'; +} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const BIDDER_CODE = 'appnexus'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -114,6 +110,7 @@ export const spec = { { code: 'beintoo', gvlid: 618 }, { code: 'projectagora', gvlid: 1032 }, { code: 'uol', gvlid: 32 }, + { code: 'adzymic', gvlid: 32 }, ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -788,7 +785,7 @@ function bidToTag(bid) { tag.keywords = auKeywords; } - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + let gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); if (gpid) { tag.gpid = gpid; } @@ -1031,7 +1028,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -1057,7 +1054,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration @@ -1142,7 +1139,7 @@ function outstreamRender(bid, doc) { hideSASIframe(bid.adUnitCode); // push to render queue because ANOutstreamVideo may not be loaded yet bid.renderer.push(() => { - const win = getWindowFromDocument(doc) || window; + const win = doc?.defaultView || window; win.ANOutstreamVideo.renderAd({ tagId: bid.adResponse.tag_id, sizes: [bid.getSize().split('x')], diff --git a/modules/asoBidAdapter.js b/modules/asoBidAdapter.js index e569f04a2a8..704cffefb39 100644 --- a/modules/asoBidAdapter.js +++ b/modules/asoBidAdapter.js @@ -7,14 +7,14 @@ import { isArray, isFn, logWarn, - parseSizesInput, - tryAppendQueryString + parseSizesInput } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { Renderer } from '../src/Renderer.js'; import { parseDomain } from '../src/refererDetection.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'aso'; const DEFAULT_SERVER_URL = 'https://srv.aso1.net'; diff --git a/modules/asteriobidAnalyticsAdapter.js b/modules/asteriobidAnalyticsAdapter.js new file mode 100644 index 00000000000..516a3a65667 --- /dev/null +++ b/modules/asteriobidAnalyticsAdapter.js @@ -0,0 +1,336 @@ +import { generateUUID, getParameterByName, logError, logInfo, parseUrl } from '../src/utils.js' +import { ajaxBuilder } from '../src/ajax.js' +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js' +import adapterManager from '../src/adapterManager.js' +import { getStorageManager } from '../src/storageManager.js' +import CONSTANTS from '../src/constants.json' +import { MODULE_TYPE_ANALYTICS } from '../src/activities/modules.js' +import {getRefererInfo} from '../src/refererDetection.js'; + +/** + * asteriobidAnalyticsAdapter.js - analytics adapter for AsterioBid + */ +export const storage = getStorageManager({ moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'asteriobid' }) +const DEFAULT_EVENT_URL = 'https://endpt.asteriobid.com/endpoint' +const analyticsType = 'endpoint' +const analyticsName = 'AsterioBid Analytics' +const utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'] +const _VERSION = 1 + +let ajax = ajaxBuilder(20000) +let initOptions +let auctionStarts = {} +let auctionTimeouts = {} +let sampling +let pageViewId +let flushInterval +let eventQueue = [] +let asteriobidAnalyticsEnabled = false + +let asteriobidAnalytics = Object.assign(adapter({ url: DEFAULT_EVENT_URL, analyticsType }), { + track({ eventType, args }) { + handleEvent(eventType, args) + } +}) + +asteriobidAnalytics.originEnableAnalytics = asteriobidAnalytics.enableAnalytics +asteriobidAnalytics.enableAnalytics = function (config) { + initOptions = config.options || {} + + pageViewId = initOptions.pageViewId || generateUUID() + sampling = initOptions.sampling || 1 + + if (Math.floor(Math.random() * sampling) === 0) { + asteriobidAnalyticsEnabled = true + flushInterval = setInterval(flush, 1000) + } else { + logInfo(`${analyticsName} isn't enabled because of sampling`) + } + + asteriobidAnalytics.originEnableAnalytics(config) +} + +asteriobidAnalytics.originDisableAnalytics = asteriobidAnalytics.disableAnalytics +asteriobidAnalytics.disableAnalytics = function () { + if (!asteriobidAnalyticsEnabled) { + return + } + flush() + clearInterval(flushInterval) + asteriobidAnalytics.originDisableAnalytics() +} + +function collectUtmTagData() { + let newUtm = false + let pmUtmTags = {} + try { + utmTags.forEach(function (utmKey) { + let utmValue = getParameterByName(utmKey) + if (utmValue !== '') { + newUtm = true + } + pmUtmTags[utmKey] = utmValue + }) + if (newUtm === false) { + utmTags.forEach(function (utmKey) { + let itemValue = storage.getDataFromLocalStorage(`pm_${utmKey}`) + if (itemValue && itemValue.length !== 0) { + pmUtmTags[utmKey] = itemValue + } + }) + } else { + utmTags.forEach(function (utmKey) { + storage.setDataInLocalStorage(`pm_${utmKey}`, pmUtmTags[utmKey]) + }) + } + } catch (e) { + logError(`${analyticsName} Error`, e) + pmUtmTags['error_utm'] = 1 + } + return pmUtmTags +} + +function collectPageInfo() { + const pageInfo = { + domain: window.location.hostname, + } + if (document.referrer) { + pageInfo.referrerDomain = parseUrl(document.referrer).hostname + } + + const refererInfo = getRefererInfo() + pageInfo.page = refererInfo.page + pageInfo.ref = refererInfo.ref + + return pageInfo +} + +function flush() { + if (!asteriobidAnalyticsEnabled) { + return + } + + if (eventQueue.length > 0) { + const data = { + pageViewId: pageViewId, + ver: _VERSION, + bundleId: initOptions.bundleId, + events: eventQueue, + utmTags: collectUtmTagData(), + pageInfo: collectPageInfo(), + sampling: sampling + } + eventQueue = [] + + if ('version' in initOptions) { + data.version = initOptions.version + } + if ('tcf_compliant' in initOptions) { + data.tcf_compliant = initOptions.tcf_compliant + } + if ('adUnitDict' in initOptions) { + data.adUnitDict = initOptions.adUnitDict; + } + if ('customParam' in initOptions) { + data.customParam = initOptions.customParam; + } + + const url = initOptions.url ? initOptions.url : DEFAULT_EVENT_URL + ajax( + url, + () => logInfo(`${analyticsName} sent events batch`), + _VERSION + ':' + JSON.stringify(data), + { + contentType: 'text/plain', + method: 'POST', + withCredentials: true + } + ) + } +} + +function trimAdUnit(adUnit) { + if (!adUnit) return adUnit + const res = {} + res.code = adUnit.code + res.sizes = adUnit.sizes + return res +} + +function trimBid(bid) { + if (!bid) return bid + const res = {} + res.auctionId = bid.auctionId + res.bidder = bid.bidder + res.bidderRequestId = bid.bidderRequestId + res.bidId = bid.bidId + res.crumbs = bid.crumbs + res.cpm = bid.cpm + res.currency = bid.currency + res.mediaTypes = bid.mediaTypes + res.sizes = bid.sizes + res.transactionId = bid.transactionId + res.adUnitCode = bid.adUnitCode + res.bidRequestsCount = bid.bidRequestsCount + res.serverResponseTimeMs = bid.serverResponseTimeMs + return res +} + +function trimBidderRequest(bidderRequest) { + if (!bidderRequest) return bidderRequest + const res = {} + res.auctionId = bidderRequest.auctionId + res.auctionStart = bidderRequest.auctionStart + res.bidderRequestId = bidderRequest.bidderRequestId + res.bidderCode = bidderRequest.bidderCode + res.bids = bidderRequest.bids && bidderRequest.bids.map(trimBid) + return res +} + +function handleEvent(eventType, eventArgs) { + if (!asteriobidAnalyticsEnabled) { + return + } + + try { + eventArgs = eventArgs ? JSON.parse(JSON.stringify(eventArgs)) : {} + } catch (e) { + // keep eventArgs as is + } + + const pmEvent = {} + pmEvent.timestamp = eventArgs.timestamp || Date.now() + pmEvent.eventType = eventType + + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.timeout = eventArgs.timeout + pmEvent.adUnits = eventArgs.adUnits && eventArgs.adUnits.map(trimAdUnit) + pmEvent.bidderRequests = eventArgs.bidderRequests && eventArgs.bidderRequests.map(trimBidderRequest) + auctionStarts[pmEvent.auctionId] = pmEvent.timestamp + auctionTimeouts[pmEvent.auctionId] = pmEvent.timeout + break + } + case CONSTANTS.EVENTS.AUCTION_END: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.end = eventArgs.end + pmEvent.start = eventArgs.start + pmEvent.adUnitCodes = eventArgs.adUnitCodes + pmEvent.bidsReceived = eventArgs.bidsReceived && eventArgs.bidsReceived.map(trimBid) + pmEvent.start = auctionStarts[pmEvent.auctionId] + pmEvent.end = Date.now() + break + } + case CONSTANTS.EVENTS.BID_ADJUSTMENT: { + break + } + case CONSTANTS.EVENTS.BID_TIMEOUT: { + pmEvent.bidders = eventArgs && eventArgs.map ? eventArgs.map(trimBid) : eventArgs + pmEvent.duration = auctionTimeouts[pmEvent.auctionId] + break + } + case CONSTANTS.EVENTS.BID_REQUESTED: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.doneCbCallCount = eventArgs.doneCbCallCount + pmEvent.start = eventArgs.start + pmEvent.bidderRequestId = eventArgs.bidderRequestId + pmEvent.bids = eventArgs.bids && eventArgs.bids.map(trimBid) + pmEvent.auctionStart = eventArgs.auctionStart + pmEvent.timeout = eventArgs.timeout + break + } + case CONSTANTS.EVENTS.BID_RESPONSE: { + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.width = eventArgs.width + pmEvent.height = eventArgs.height + pmEvent.adId = eventArgs.adId + pmEvent.mediaType = eventArgs.mediaType + pmEvent.cpm = eventArgs.cpm + pmEvent.currency = eventArgs.currency + pmEvent.requestId = eventArgs.requestId + pmEvent.adUnitCode = eventArgs.adUnitCode + pmEvent.auctionId = eventArgs.auctionId + pmEvent.timeToRespond = eventArgs.timeToRespond + pmEvent.requestTimestamp = eventArgs.requestTimestamp + pmEvent.responseTimestamp = eventArgs.responseTimestamp + pmEvent.netRevenue = eventArgs.netRevenue + pmEvent.size = eventArgs.size + pmEvent.adserverTargeting = eventArgs.adserverTargeting + break + } + case CONSTANTS.EVENTS.BID_WON: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.adId = eventArgs.adId + pmEvent.adserverTargeting = eventArgs.adserverTargeting + pmEvent.adUnitCode = eventArgs.adUnitCode + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.height = eventArgs.height + pmEvent.mediaType = eventArgs.mediaType + pmEvent.netRevenue = eventArgs.netRevenue + pmEvent.cpm = eventArgs.cpm + pmEvent.requestTimestamp = eventArgs.requestTimestamp + pmEvent.responseTimestamp = eventArgs.responseTimestamp + pmEvent.size = eventArgs.size + pmEvent.width = eventArgs.width + pmEvent.currency = eventArgs.currency + pmEvent.bidder = eventArgs.bidder + break + } + case CONSTANTS.EVENTS.BIDDER_DONE: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.auctionStart = eventArgs.auctionStart + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.bidderRequestId = eventArgs.bidderRequestId + pmEvent.bids = eventArgs.bids && eventArgs.bids.map(trimBid) + pmEvent.doneCbCallCount = eventArgs.doneCbCallCount + pmEvent.start = eventArgs.start + pmEvent.timeout = eventArgs.timeout + pmEvent.tid = eventArgs.tid + pmEvent.src = eventArgs.src + break + } + case CONSTANTS.EVENTS.SET_TARGETING: { + break + } + case CONSTANTS.EVENTS.REQUEST_BIDS: { + break + } + case CONSTANTS.EVENTS.ADD_AD_UNITS: { + break + } + case CONSTANTS.EVENTS.AD_RENDER_FAILED: { + pmEvent.bid = eventArgs.bid + pmEvent.message = eventArgs.message + pmEvent.reason = eventArgs.reason + break + } + default: + return + } + + sendEvent(pmEvent) +} + +function sendEvent(event) { + eventQueue.push(event) + logInfo(`${analyticsName} Event ${event.eventType}:`, event) + + if (event.eventType === CONSTANTS.EVENTS.AUCTION_END) { + flush() + } +} + +adapterManager.registerAnalyticsAdapter({ + adapter: asteriobidAnalytics, + code: 'asteriobid' +}) + +asteriobidAnalytics.getOptions = function () { + return initOptions +} + +asteriobidAnalytics.flush = flush + +export default asteriobidAnalytics diff --git a/modules/asteriobidAnalyticsAdapter.md b/modules/asteriobidAnalyticsAdapter.md new file mode 100644 index 00000000000..524cf6e2721 --- /dev/null +++ b/modules/asteriobidAnalyticsAdapter.md @@ -0,0 +1,41 @@ +# Overview + +Module Name: AsterioBid Analytics Adapter +Module Type: Analytics Adapter +Maintainer: admin@asteriobid.com + +# Description +Analytics adapter for AsterioBid. Contact admin@asteriobid.com for information. + +# Test Parameters + +``` +pbjs.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: '04bcf17b-9733-4675-9f67-d475f881ab78' + } +}); + +``` + +# Advanced Parameters + +``` +pbjs.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: '04bcf17b-9733-4675-9f67-d475f881ab78', + version: 'v1', // configuration version for the comparison + adUnitDict: { // provide names of the ad units for better reporting + adunitid1: 'Top Banner', + adunitid2: 'Bottom Banner' + }, + customParam: { // provide custom parameters values that you want to collect and report + param1: 'value1', + param2: 'value2' + } + } +}); + +``` diff --git a/modules/audiencerunBidAdapter.js b/modules/audiencerunBidAdapter.js index 9beb20d4f77..e716fe94c8b 100644 --- a/modules/audiencerunBidAdapter.js +++ b/modules/audiencerunBidAdapter.js @@ -1,8 +1,7 @@ import { _each, deepAccess, - formatQS, - getBidIdParameter, + formatQS, getBidIdParameter, getValue, isArray, isFn, diff --git a/modules/automatadAnalyticsAdapter.js b/modules/automatadAnalyticsAdapter.js new file mode 100644 index 00000000000..7d7bd8cb34c --- /dev/null +++ b/modules/automatadAnalyticsAdapter.js @@ -0,0 +1,325 @@ +import { + logError, + logInfo, + logMessage +} from '../src/utils.js'; + +import CONSTANTS from '../src/constants.json'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import { config } from '../src/config.js' + +/** Prebid Event Handlers */ + +const ADAPTER_CODE = 'automatadAnalytics' +const trialCountMilsMapping = [1500, 3000, 5000, 10000]; + +var isLoggingEnabled; var queuePointer = 0; var retryCount = 0; var timer = null; var __atmtdAnalyticsQueue = []; + +const prettyLog = (level, text, isGroup = false, cb = () => {}) => { + if (self.isLoggingEnabled === undefined) { + if (window.localStorage.getItem('__aggLoggingEnabled')) { + self.isLoggingEnabled = true + } else { + const queryParams = new URLSearchParams(new URL(window.location.href).search) + self.isLoggingEnabled = queryParams.has('aggLoggingEnabled') + } + } + + if (self.isLoggingEnabled) { + if (isGroup) { + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text} --- Group Start ---`) + try { + cb(); + } catch (error) { + logError(`ATD Analytics Adapter: ERROR: ${'Error during cb function in prettyLog'}`) + } + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text} --- Group End ---`) + } else { + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text}`) + } + } +} + +const processEvents = () => { + if (self.retryCount === trialCountMilsMapping.length) { + self.prettyLog('error', `Aggregator still hasn't loaded. Processing que stopped`, trialCountMilsMapping, self.retryCount) + return; + } + + self.prettyLog('status', `Que has been inactive for a while. Adapter starting to process que now... Trial Count = ${self.retryCount + 1}`) + + let shouldTryAgain = false + + while (self.queuePointer < self.__atmtdAnalyticsQueue.length) { + const eventType = self.__atmtdAnalyticsQueue[self.queuePointer][0] + const args = self.__atmtdAnalyticsQueue[self.queuePointer][1] + + try { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionInitHandler) { + window.atmtdAnalytics.auctionInitHandler(args); + } else { + shouldTryAgain = true + } + break; + case CONSTANTS.EVENTS.BID_REQUESTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRequestedHandler) { + window.atmtdAnalytics.bidRequestedHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidResponseHandler) { + window.atmtdAnalytics.bidResponseHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_REJECTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRejectedHandler) { + window.atmtdAnalytics.bidRejectedHandler(args); + } + break; + case CONSTANTS.EVENTS.BIDDER_DONE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderDoneHandler) { + window.atmtdAnalytics.bidderDoneHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_WON: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidWonHandler) { + window.atmtdAnalytics.bidWonHandler(args); + } + break; + case CONSTANTS.EVENTS.NO_BID: + if (window.atmtdAnalytics && window.atmtdAnalytics.noBidHandler) { + window.atmtdAnalytics.noBidHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_TIMEOUT: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderTimeoutHandler) { + window.atmtdAnalytics.bidderTimeoutHandler(args); + } + break; + case CONSTANTS.EVENTS.AUCTION_DEBUG: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionDebugHandler) { + window.atmtdAnalytics.auctionDebugHandler(args); + } + break; + case 'slotRenderEnded': + if (window.atmtdAnalytics && window.atmtdAnalytics.slotRenderEndedGPTHandler) { + window.atmtdAnalytics.slotRenderEndedGPTHandler(args); + } else { + shouldTryAgain = true + } + break; + case 'impressionViewable': + if (window.atmtdAnalytics && window.atmtdAnalytics.impressionViewableHandler) { + window.atmtdAnalytics.impressionViewableHandler(args); + } else { + shouldTryAgain = true + } + break; + } + + if (shouldTryAgain) break; + } catch (error) { + self.prettyLog('error', `Unhandled Error while processing ${eventType} of ${self.queuePointer}th index in the que. Will not be retrying this raw event ...`, true, () => { + logError(`The error is `, error) + }) + } + + self.queuePointer = self.queuePointer + 1 + } + + if (shouldTryAgain) { + if (trialCountMilsMapping[self.retryCount]) self.prettyLog('warn', `Adapter failed to process event as aggregator has not loaded. Retrying in ${trialCountMilsMapping[self.retryCount]}ms ...`); + setTimeout(self.processEvents, trialCountMilsMapping[self.retryCount]) + self.retryCount = self.retryCount + 1 + } +} + +const addGPTHandlers = () => { + const googletag = window.googletag || {} + googletag.cmd = googletag.cmd || [] + googletag.cmd.push(() => { + googletag.pubads().addEventListener('slotRenderEnded', (event) => { + if (window.atmtdAnalytics && window.atmtdAnalytics.slotRenderEndedGPTHandler) { + if (window.__atmtdAggregatorFirstAuctionInitialized === true) { + window.atmtdAnalytics.slotRenderEndedGPTHandler(event) + return; + } + } + self.__atmtdAnalyticsQueue.push(['slotRenderEnded', event]) + self.prettyLog(`warn`, `Aggregator not initialised at auctionInit, exiting slotRenderEnded handler and pushing to que instead`) + }) + + googletag.pubads().addEventListener('impressionViewable', (event) => { + if (window.atmtdAnalytics && window.atmtdAnalytics.impressionViewableHandler) { + if (window.__atmtdAggregatorFirstAuctionInitialized === true) { + window.atmtdAnalytics.impressionViewableHandler(event) + return; + } + } + self.__atmtdAnalyticsQueue.push(['impressionViewable', event]) + self.prettyLog(`warn`, `Aggregator not initialised at auctionInit, exiting impressionViewable handler and pushing to que instead`) + }) + }) +} + +const initializeQueue = () => { + self.__atmtdAnalyticsQueue.push = (args) => { + Array.prototype.push.apply(self.__atmtdAnalyticsQueue, [args]); + if (timer) { + clearTimeout(timer); + timer = null; + } + + if (args[0] === CONSTANTS.EVENTS.AUCTION_INIT) { + const timeout = parseInt(config.getConfig('bidderTimeout')) + 1500 + timer = setTimeout(() => { + self.processEvents() + }, timeout); + } else { + timer = setTimeout(() => { + self.processEvents() + }, 1500); + } + }; +} + +// ANALYTICS ADAPTER + +let baseAdapter = adapter({analyticsType: 'bundle'}); +let atmtdAdapter = Object.assign({}, baseAdapter, { + + disableAnalytics() { + baseAdapter.disableAnalytics.apply(this, arguments); + }, + + track({eventType, args}) { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionInitHandler) { + self.prettyLog('status', 'Aggregator loaded, initialising auction through handlers'); + window.atmtdAnalytics.auctionInitHandler(args); + } else { + self.prettyLog('warn', 'Aggregator not loaded, initialising auction through que ...'); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_REQUESTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRequestedHandler) { + window.atmtdAnalytics.bidRequestedHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_REJECTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRejectedHandler) { + window.atmtdAnalytics.bidRejectedHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidResponseHandler) { + window.atmtdAnalytics.bidResponseHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BIDDER_DONE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderDoneHandler) { + window.atmtdAnalytics.bidderDoneHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_WON: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidWonHandler) { + window.atmtdAnalytics.bidWonHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.NO_BID: + if (window.atmtdAnalytics && window.atmtdAnalytics.noBidHandler) { + window.atmtdAnalytics.noBidHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.AUCTION_DEBUG: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionDebugHandler) { + window.atmtdAnalytics.auctionDebugHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_TIMEOUT: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderTimeoutHandler) { + window.atmtdAnalytics.bidderTimeoutHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + } + } +}); + +atmtdAdapter.originEnableAnalytics = atmtdAdapter.enableAnalytics + +atmtdAdapter.enableAnalytics = function (configuration) { + if ((configuration === undefined && typeof configuration !== 'object') || configuration.options === undefined) { + logError('A valid configuration must be passed to the Atmtd Analytics Adapter.'); + return; + } + + const conf = configuration.options + + if (conf === undefined || typeof conf !== 'object' || conf.siteID === undefined || conf.publisherID === undefined) { + logError('A valid publisher ID and siteID must be passed to the Atmtd Analytics Adapter.'); + return; + } + + self.initializeQueue() + self.addGPTHandlers() + + window.__atmtdSDKConfig = { + publisherID: conf.publisherID, + siteID: conf.siteID, + collectDebugMessages: conf.logDebug ? conf.logDebug : false + } + + logMessage(`Automatad Analytics Adapter enabled with sdk config`, window.__atmtdSDKConfig) + + // eslint-disable-next-line + atmtdAdapter.originEnableAnalytics(configuration) +}; + +/// /////////// ADAPTER REGISTRATION ////////////// + +adapterManager.registerAnalyticsAdapter({ + adapter: atmtdAdapter, + code: ADAPTER_CODE +}); + +export var self = { + __atmtdAnalyticsQueue, + processEvents, + initializeQueue, + addGPTHandlers, + prettyLog, + queuePointer, + retryCount, + isLoggingEnabled +} + +export default atmtdAdapter; diff --git a/modules/automatadAnalyticsAdapter.md b/modules/automatadAnalyticsAdapter.md new file mode 100644 index 00000000000..2be1af87f20 --- /dev/null +++ b/modules/automatadAnalyticsAdapter.md @@ -0,0 +1,23 @@ + +# Overview + +Module Name: Automatad Analytics Adapter +Module Type: Analytics Adapter +Maintainer: tech@automatad.com + +# Description + +Analytics adapter for automatad.com. Contact tech@automatad.com for information. + +# Test Parameters + +``` +{ + provider: 'automatadAnalytics', + options: { + publisherID: 'N8vZLx', + siteID: 'PXfvBq' + } +} + +``` \ No newline at end of file diff --git a/modules/axisBidAdapter.js b/modules/axisBidAdapter.js new file mode 100644 index 00000000000..8d7f2dd04fd --- /dev/null +++ b/modules/axisBidAdapter.js @@ -0,0 +1,210 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'axis'; +const AD_URL = 'https://prebid.axis-marketplace.com/pbjs'; +const SYNC_URL = 'https://cs.axis-marketplace.com'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { integration, token } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + integration, + token, + bidId, + schain, + bidfloor + }; + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.pos = mediaTypes[BANNER].pos; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.pos = mediaTypes[VIDEO].pos; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + placement.context = mediaTypes[VIDEO].context; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (e) { + logError(e); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && params.integration && params.token); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + iabCat: deepAccess(bidderRequest, 'ortb2.site.cat'), + coppa: deepAccess(bidderRequest, 'ortb2.regs.coppa') ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout || 3000, + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/axisBidAdapter.md b/modules/axisBidAdapter.md new file mode 100644 index 00000000000..d1625a56176 --- /dev/null +++ b/modules/axisBidAdapter.md @@ -0,0 +1,83 @@ +# Overview + +``` +Module Name: Axis Bidder Adapter +Module Type: Axis Bidder Adapter +Maintainer: help@axis-marketplace.com +``` + +# Description + +Connects to Axis exchange for bids. +Axis bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + pos: 1 + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + minduration: 5, + maxduration: 60, + pos: 1 + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + } + ]; +``` diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 37de8e637a9..658fc30b43b 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -7,14 +7,15 @@ import { isFn, logWarn, parseSizesInput, - parseUrl + parseUrl, + formatQS } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {Renderer} from '../src/Renderer.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; -const ADAPTER_VERSION = '1.19'; +const ADAPTER_VERSION = '1.20'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; const CURRENCY = 'USD'; @@ -22,6 +23,8 @@ const CURRENCY = 'USD'; export const VIDEO_ENDPOINT = 'https://reachms.bfmio.com/bid.json?exchange_id='; export const BANNER_ENDPOINT = 'https://display.bfmio.com/prebid_display'; export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; +export const SYNC_IFRAME_ENDPOINT = 'https://sync.bfmio.com/sync_iframe'; +export const SYNC_IMAGE_ENDPOINT = 'https://sync.bfmio.com/syncb'; export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'placement', 'skip', 'skipmin', 'skipafter']; export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; @@ -152,11 +155,22 @@ export const spec = { } }, - getUserSyncs(syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '') { - let syncs = []; + getUserSyncs(syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = {}) { let { gdprApplies, consentString = '' } = gdprConsent; + let { gppString = '', applicableSections = [] } = gppConsent; let bannerResponse = find(serverResponses, (res) => isArray(res.body)); + let syncs = []; + let params = { + id: appId, + gdpr: gdprApplies ? 1 : 0, + gc: consentString, + gce: 1, + us_privacy: uspConsent, + gpp: gppString, + gpp_sid: Array.isArray(applicableSections) ? applicableSections.join(',') : '' + }; + if (bannerResponse) { if (syncOptions.iframeEnabled) { bannerResponse.body @@ -171,12 +185,12 @@ export const spec = { } else if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: `https://sync.bfmio.com/sync_iframe?ifg=1&id=${appId}&gdpr=${gdprApplies ? 1 : 0}&gc=${consentString}&gce=1&us_privacy=${uspConsent}` + url: `${SYNC_IFRAME_ENDPOINT}?ifg=1&${formatQS(params)}` }); } else if (syncOptions.pixelEnabled) { syncs.push({ type: 'image', - url: `https://sync.bfmio.com/syncb?pid=144&id=${appId}&gdpr=${gdprApplies ? 1 : 0}&gc=${consentString}&gce=1&us_privacy=${uspConsent}` + url: `${SYNC_IMAGE_ENDPOINT}?pid=144&${formatQS(params)}` }); } @@ -404,6 +418,12 @@ function createVideoRequestData(bid, bidderRequest) { deepSetValue(payload, 'user.ext.consent', consentString); } + if (bidderRequest && bidderRequest.gppConsent) { + let { gppString, applicableSections } = bidderRequest.gppConsent; + deepSetValue(payload, 'regs.gpp', gppString); + deepSetValue(payload, 'regs.gpp_sid', applicableSections); + } + if (bid.schain) { deepSetValue(payload, 'source.ext.schain', bid.schain); } @@ -459,6 +479,12 @@ function createBannerRequestData(bids, bidderRequest) { payload.gdprConsent = consentString; } + if (bidderRequest && bidderRequest.gppConsent) { + let { gppString, applicableSections } = bidderRequest.gppConsent; + payload.gpp = gppString; + payload.gppSid = applicableSections; + } + if (bids[0] && bids[0].schain) { payload.schain = bids[0].schain; } diff --git a/modules/beopBidAdapter.js b/modules/beopBidAdapter.js index 6348854ec2a..b6b6107ddd0 100644 --- a/modules/beopBidAdapter.js +++ b/modules/beopBidAdapter.js @@ -1,7 +1,6 @@ import { buildUrl, - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, getValue, isArray, logInfo, @@ -48,8 +47,10 @@ export const spec = { */ buildRequests: function(validBidRequests, bidderRequest) { const slots = validBidRequests.map(beOpRequestSlotsMaker); - const firstPartyData = bidderRequest.ortb2; - const psegs = (firstPartyData && firstPartyData.user && firstPartyData.user.ext && firstPartyData.user.ext.data) ? firstPartyData.user.ext.data.permutive : undefined; + const firstPartyData = bidderRequest.ortb2 || {}; + const psegs = firstPartyData.user?.ext?.permutive || firstPartyData.user?.ext?.data?.permutive || []; + const userBpSegs = firstPartyData.user?.ext?.bpsegs || firstPartyData.user?.ext?.data?.bpsegs || []; + const siteBpSegs = firstPartyData.site?.ext?.bpsegs || firstPartyData.site?.ext?.data?.bpsegs || []; const pageUrl = getPageUrl(bidderRequest.refererInfo, window); const gdpr = bidderRequest.gdprConsent; const firstSlot = slots[0]; @@ -61,6 +62,8 @@ export const spec = { nid: firstSlot.nid, nptnid: firstSlot.nptnid, pid: firstSlot.pid, + psegs: psegs, + bpsegs: (userBpSegs.concat(siteBpSegs)).map(item => item.toString()), url: pageUrl, lang: (window.navigator.language || window.navigator.languages[0]), kwds: keywords, @@ -71,10 +74,6 @@ export const spec = { tc_string: (gdpr && gdpr.gdprApplies) ? gdpr.consentString : null, }; - if (psegs) { - Object.assign(payloadObject, {psegs: psegs}); - } - const payloadString = JSON.stringify(payloadObject); return { method: 'POST', @@ -129,8 +128,6 @@ function buildTrackingParams(data, info, value) { nptnid: params.networkPartnerId, bid: data.bidId || data.requestId, sl_n: data.adUnitCode, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - aid: data.auctionId, se_ca: 'bid', se_ac: info, se_va: value, @@ -158,8 +155,6 @@ function beOpRequestSlotsMaker(bid) { bid: getBidIdParameter('bidId', bid), brid: getBidIdParameter('bidderRequestId', bid), name: getBidIdParameter('adUnitCode', bid), - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - aid: getBidIdParameter('auctionId', bid), tid: bid.ortb2Imp?.ext?.tid || '', brc: getBidIdParameter('bidRequestsCount', bid), bdrc: getBidIdParameter('bidderRequestCount', bid), diff --git a/modules/beopBidAdapter.md b/modules/beopBidAdapter.md index c0e88cb1ceb..53d2542b69c 100644 --- a/modules/beopBidAdapter.md +++ b/modules/beopBidAdapter.md @@ -9,13 +9,14 @@ Module that connects to BeOp's demand sources # Test Parameters + ``` var adUnits = [ { code: 'in-article', mediaTypes: { banner: { - sizes: [[1,1]], + sizes: [[1,1]], } }, bids: [ @@ -31,3 +32,36 @@ Module that connects to BeOp's demand sources ]; ``` +# Custom Bidder data + +If you want to pass your first party data to BeOp, you can set your bidder `config.ortb2` object with + +```json +{ + "site": { + "ext": { + "bpsegs": ["Your", 1, "ST", "party", "data"], + "data": { + "bpsegs": ["Your", 1, "ST", "party", "data"] + } + } + }, + "user": { + "ext": { + "bpsegs": ["Your", 1, "ST", "party", "data"], + "data": { + "bpsegs": ["Your", 1, "ST", "party", "data"] + } + } + } +} +``` + +You can choose the location between: + +- `site.ext` +- `site.ext.data` +- `user.ext` +- `user.ext.data` + +and our BidAdapter will be able to find them diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index d615e433cc0..6883b7cce2c 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -1,6 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {getAdUnitSizes, parseSizesInput} from '../src/utils.js'; +import {parseSizesInput} from '../src/utils.js'; import {includes} from '../src/polyfill.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'between'; let ENDPOINT = 'https://ads.betweendigital.com/adjson?t=prebid'; diff --git a/modules/bidViewability.js b/modules/bidViewability.js index a5cab99b1a7..be18095e369 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -67,6 +67,8 @@ export let logWinningBidNotFound = (slot) => { export let impressionViewableHandler = (globalModuleConfig, slot, event) => { let respectiveBid = getMatchingWinningBidForGPTSlot(globalModuleConfig, slot); + let respectiveDeferredAdUnit = getGlobal().adUnits.find(adUnit => adUnit.deferBilling && respectiveBid.adUnitCode === adUnit.code); + if (respectiveBid === null) { logWinningBidNotFound(slot); } else { @@ -74,6 +76,11 @@ export let impressionViewableHandler = (globalModuleConfig, slot, event) => { fireViewabilityPixels(globalModuleConfig, respectiveBid); // trigger respective bidder's onBidViewable handler adapterManager.callBidViewableBidder(respectiveBid.adapterCode || respectiveBid.bidder, respectiveBid); + + if (respectiveDeferredAdUnit) { + adapterManager.callBidBillableBidder(respectiveBid); + } + // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels events.emit(CONSTANTS.EVENTS.BID_VIEWABLE, respectiveBid); } diff --git a/modules/bidViewability.md b/modules/bidViewability.md index 78a1539fb1a..922a4a9def4 100644 --- a/modules/bidViewability.md +++ b/modules/bidViewability.md @@ -2,19 +2,20 @@ Module Name: bidViewability -Purpose: Track when a bid is viewable +Purpose: Track when a bid is viewable (and also ready for billing) Maintainer: harshad.mane@pubmatic.com # Description -- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Analytics adapters, bidders will need to implement `onBidViewable` method to capture this event -- Bidderes can check if this module is part of the final build and whether it is enabled or not by accessing ```pbjs.getConfig('bidViewability')``` +- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Analytics adapters, bidders will need to implement the `onBidViewable` method to capture this event +- Bidders can check if this module is part of the final build and whether it is enabled or not by accessing ```pbjs.getConfig('bidViewability')``` - GPT API is used to find when a bid is viewable, https://developers.google.com/publisher-tag/reference#googletag.events.impressionviewableevent . This event is fired when an impression becomes viewable, according to the Active View criteria. Refer: https://support.google.com/admanager/answer/4524488 -- The module does not work with adserver other than GAM with GPT integration +- This module does not work with any adserver's other than GAM with GPT integration - Logic used to find a matching pbjs-bid for a GPT slot is ``` (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode) ``` this logic can be changed by using param ```customMatchFunction``` -- When a rendered PBJS bid is viewable the module will trigger BID_VIEWABLE event, which can be consumed by bidders and analytics adapters -- For the viewable bid if ```bid.vurls type array``` param is and module config ``` firePixels: true ``` is set then the URLs mentioned in bid.vurls will be executed. Please note that GDPR and USP related parameters will be added to the given URLs +- When a rendered PBJS bid is viewable the module will trigger a BID_VIEWABLE event, which can be consumed by bidders and analytics adapters +- If the viewable bid contains a ```vurls``` param containing URL's and the Bid Viewability module is configured with ``` firePixels: true ``` then the URLs mentioned in bid.vurls will be called. Please note that GDPR and USP related parameters will be added to the given URLs +- This module is also compatible with Prebid core's billing deferral logic, this means that bids linked to an ad unit marked with `deferBilling: true` will trigger a bid adapter's `onBidBillable` function (if present) indicating an ad slot was viewed and also billing ready (if it were deferred). # Params - enabled [required] [type: boolean, default: false], when set to true, the module will emit BID_VIEWABLE when applicable @@ -44,6 +45,6 @@ Refer: https://support.google.com/admanager/answer/4524488 ``` # Please Note: -- Doesn't seems to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html as GPT's impressionViewable event is not triggered for instream-video-creative -- Works with Banner, Outsteam, Native creatives +- This module doesn't seem to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html as GPT's impressionViewable event is not triggered for instream-video-creative +- Works with Banner, Outsteam and Native creatives diff --git a/modules/bidglassBidAdapter.js b/modules/bidglassBidAdapter.js index 3184372881b..a29976cfcb7 100644 --- a/modules/bidglassBidAdapter.js +++ b/modules/bidglassBidAdapter.js @@ -1,4 +1,4 @@ -import { _each, isArray, getBidIdParameter, deepClone, getUniqueIdentifierStr } from '../src/utils.js'; +import {_each, isArray, deepClone, getUniqueIdentifierStr, getBidIdParameter} from '../src/utils.js'; // import {config} from 'src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; diff --git a/modules/bliinkBidAdapter.js b/modules/bliinkBidAdapter.js index 9a9d74d14c1..e674c6987c3 100644 --- a/modules/bliinkBidAdapter.js +++ b/modules/bliinkBidAdapter.js @@ -2,8 +2,9 @@ // eslint-disable-next-line prebid/validate-imports import { registerBidder } from '../src/adapters/bidderFactory.js' import { config } from '../src/config.js' -import {_each, deepAccess, deepSetValue} from '../src/utils.js' +import { _each, deepAccess, deepSetValue, getWindowSelf, getWindowTop } from '../src/utils.js' export const BIDDER_CODE = 'bliink' +export const GVL_ID = 658 export const BLIINK_ENDPOINT_ENGINE = 'https://engine.bliink.io/prebid' export const BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME = 'https://tag.bliink.io/usersync.html' @@ -12,9 +13,10 @@ export const META_DESCRIPTION = 'description' const VIDEO = 'video' const BANNER = 'banner' - +window.bliinkBid = window.bliinkBid || {}; const supportedMediaTypes = [BANNER, VIDEO] const aliasBidderCode = ['bk'] +const CURRENCY = 'EUR'; /** * @description get coppa value from config @@ -23,6 +25,36 @@ function getCoppa() { return config.getConfig('coppa') === true ? 1 : 0; } +/** + * Retrieves the effective connection type from the browser's Navigator API. + * @returns {string} The effective connection type or 'unsupported' if unavailable. + */ +export function getEffectiveConnectionType() { + /** + * The effective connection type obtained from the browser's Navigator API. + * @type {string|undefined} + */ + const navigatorEffectiveType = navigator?.connection?.effectiveType; + + if (navigatorEffectiveType) { + return navigatorEffectiveType; + } + + return 'unsupported'; +} + +/** + * Retrieves the user IDs as EIDs from the first valid bid request. + * + * @param {Array} validBidRequests - Array of valid bid requests + * @returns {Array|undefined} - Array of user IDs as EIDs, or undefined if not found + */ +export function getUserIds(validBidRequests) { + /** @type {Object} */ + if (validBidRequests?.[0]?.userIdAsEids) { + return validBidRequests[0].userIdAsEids; + } +} export function getMetaList(name) { if (!name || name.length === 0) return [] @@ -91,6 +123,35 @@ export function getKeywords() { return []; } +function canAccessTopWindow() { + try { + if (getWindowTop().location.href) { + return true; + } + } catch (error) { + return false; + } +} + +/** + * domLoading feature is computed on window.top if reachable. + */ +export function getDomLoadingDuration() { + let domLoadingDuration = -1; + let performance; + + performance = (canAccessTopWindow()) ? getWindowTop().performance : getWindowSelf().performance; + + if (performance && performance.timing && performance.timing.navigationStart > 0) { + const val = performance.timing.domLoading - performance.timing.navigationStart; + if (val > 0) { + domLoadingDuration = val; + } + } + + return domLoadingDuration; +} + /** * @param bidRequest * @return {({cpm, netRevenue: boolean, requestId, width: number, currency, ttl: number, creativeId, height: number}&{mediaType: string, vastXml})|null} @@ -120,7 +181,7 @@ export const buildBid = (bidResponse) => { } return Object.assign(bid, { cpm: bidResponse.price, - currency: bidResponse.currency || 'EUR', + currency: bidResponse.currency || CURRENCY, creativeId: deepAccess(bidResponse, 'extras.deal_id'), requestId: deepAccess(bidResponse, 'extras.transaction_id'), width: deepAccess(bidResponse, `creative.${bid.mediaType}.width`) || 1, @@ -149,29 +210,59 @@ export const isBidRequestValid = (bid) => { */ export const buildRequests = (validBidRequests, bidderRequest) => { if (!validBidRequests || !bidderRequest || !bidderRequest.bids) return null - + const domLoadingDuration = getDomLoadingDuration().toString(); const tags = bidderRequest.bids.map((bid) => { - return { + let bidFloor; + const sizes = bid.sizes.map((size) => ({ w: size[0], h: size[1] })); + const mediaTypes = Object.keys(bid.mediaTypes) + if (typeof bid.getFloor === 'function') { + bidFloor = bid.getFloor({ + currency: CURRENCY, + mediaType: mediaTypes[0], + size: sizes[0] + }); + } + const id = bid.params.tagId + const request = { sizes: bid.sizes.map((size) => ({ w: size[0], h: size[1] })), - id: bid.params.tagId, + id, // TODO: bidId is globally unique, is it a good choice for transaction ID (vs ortb2Imp.ext.tid)? transactionId: bid.bidId, - mediaTypes: Object.keys(bid.mediaTypes), + mediaTypes: mediaTypes, imageUrl: deepAccess(bid, 'params.imageUrl', ''), - }; + videoUrl: deepAccess(bid, 'params.videoUrl', ''), + refresh: (window.bliinkBid[id] = (window.bliinkBid[id] ?? -1) + 1) || undefined, + } + if (bidFloor) { + request.bidFloor = bidFloor + } + return request; }); let request = { tags, pageTitle: document.title, - pageUrl: deepAccess(bidderRequest, 'refererInfo.page'), + pageUrl: deepAccess(bidderRequest, 'refererInfo.page').replace(/\?.*$/, ''), pageDescription: getMetaValue(META_DESCRIPTION), keywords: getKeywords().join(','), + ect: getEffectiveConnectionType(), }; + const schain = deepAccess(validBidRequests[0], 'schain') + const eids = getUserIds(validBidRequests) + const device = bidderRequest.ortb2?.device if (schain) { request.schain = schain } + if (domLoadingDuration > -1) { + request.domLoadingDuration = domLoadingDuration + } + if (device) { + request.device = device + } + if (eids) { + request.eids = eids + } const gdprConsent = deepAccess(bidderRequest, 'gdprConsent'); if (!!gdprConsent && gdprConsent.gdprApplies) { request.gdpr = true @@ -183,7 +274,6 @@ export const buildRequests = (validBidRequests, bidderRequest) => { if (bidderRequest.uspConsent) { deepSetValue(request, 'uspConsent', bidderRequest.uspConsent); } - return { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, @@ -254,6 +344,7 @@ const getUserSyncs = (syncOptions, serverResponses, gdprConsent, uspConsent) => */ export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, aliases: aliasBidderCode, supportedMediaTypes: supportedMediaTypes, isBidRequestValid, diff --git a/modules/boldwinBidAdapter.js b/modules/boldwinBidAdapter.js index 3915df8b976..4d97f830d33 100644 --- a/modules/boldwinBidAdapter.js +++ b/modules/boldwinBidAdapter.js @@ -80,6 +80,15 @@ export const spec = { if (bidderRequest.gdprConsent) { request.gdpr = bidderRequest.gdprConsent; } + + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } } const len = validBidRequests.length; diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js index 53868eccc4c..bd7a33ff037 100644 --- a/modules/brandmetricsRtdProvider.js +++ b/modules/brandmetricsRtdProvider.js @@ -6,8 +6,10 @@ * @requires module:modules/realTimeData */ import {submodule} from '../src/hook.js'; -import {deepAccess, deepSetValue, logError, mergeDeep} from '../src/utils.js'; +import {deepAccess, deepSetValue, logError, mergeDeep, generateUUID} from '../src/utils.js'; import {loadExternalScript} from '../src/adloader.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; const MODULE_NAME = 'brandmetrics' const MODULE_CODE = MODULE_NAME @@ -15,14 +17,18 @@ const RECEIVED_EVENTS = [] const GVL_ID = 422 const TCF_PURPOSES = [1, 7] +let billableEventsInitialized = false + function init (config, userConsent) { const hasConsent = checkConsent(userConsent) + const initialize = hasConsent !== false - if (hasConsent) { + if (initialize) { const moduleConfig = getMergedConfig(config) initializeBrandmetrics(moduleConfig.params.scriptId) + initializeBillableEvents() } - return hasConsent + return initialize } /** @@ -31,33 +37,35 @@ function init (config, userConsent) { * @returns {boolean} */ function checkConsent (userConsent) { - let consent = false - - if (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies) { - const gdpr = userConsent.gdpr - - if (gdpr.vendorData) { - const vendor = gdpr.vendorData.vendor - const purpose = gdpr.vendorData.purpose - - let vendorConsent = false - if (vendor.consents) { - vendorConsent = vendor.consents[GVL_ID] - } - - if (vendor.legitimateInterests) { - vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID] + let consent + + if (userConsent) { + if (userConsent.gdpr && userConsent.gdpr.gdprApplies) { + const gdpr = userConsent.gdpr + + if (gdpr.vendorData) { + const vendor = gdpr.vendorData.vendor + const purpose = gdpr.vendorData.purpose + + let vendorConsent = false + if (vendor.consents) { + vendorConsent = vendor.consents[GVL_ID] + } + + if (vendor.legitimateInterests) { + vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID] + } + + const purposes = TCF_PURPOSES.map(id => { + return (purpose.consents && purpose.consents[id]) || (purpose.legitimateInterests && purpose.legitimateInterests[id]) + }) + const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length + consent = vendorConsent && purposesValid } - - const purposes = TCF_PURPOSES.map(id => { - return (purpose.consents && purpose.consents[id]) || (purpose.legitimateInterests && purpose.legitimateInterests[id]) - }) - const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length - consent = vendorConsent && purposesValid + } else if (userConsent.usp) { + const usp = userConsent.usp + consent = usp[1] !== 'N' && usp[2] !== 'Y' } - } else if (userConsent.usp) { - const usp = userConsent.usp - consent = usp[1] !== 'N' && usp[2] !== 'Y' } return consent @@ -82,7 +90,6 @@ function processBrandmetricsEvents (reqBidsConfigObj, moduleConfig, callback) { if (RECEIVED_EVENTS.length > 0) { callBidTargeting(RECEIVED_EVENTS[RECEIVED_EVENTS.length - 1]) } else { - window._brandmetrics = window._brandmetrics || [] window._brandmetrics.push({ cmd: '_addeventlistener', val: { @@ -120,6 +127,8 @@ function setBidderTargeting (reqBidsConfigObj, moduleConfig, key, val) { * @param {string} scriptId - The script- id provided by brandmetrics or brandmetrics partner */ function initializeBrandmetrics(scriptId) { + window._brandmetrics = window._brandmetrics || [] + if (scriptId) { const path = 'https://cdn.brandmetrics.com/survey/script/' const file = scriptId + '.js' @@ -129,6 +138,34 @@ function initializeBrandmetrics(scriptId) { } } +/** +* Hook in to brandmetrics creative_in_view- event and emit billable- event for creatives measured by brandmetrics. +*/ +function initializeBillableEvents() { + if (!billableEventsInitialized) { + window._brandmetrics.push({ + cmd: '_addeventlistener', + val: { + event: 'creative_in_view', + handler: (ev) => { + if (ev.source && ev.source.type === 'pbj') { + const bid = ev.source.data; + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + vendor: 'brandmetrics', + type: 'creative_in_view', + measurementId: ev.mid, + billingId: generateUUID(), + auctionId: bid.auctionId, + transactionId: bid.transactionId, + }); + } + }, + } + }) + billableEventsInitialized = true + } +} + /** * Merges a provided config with default values * @param {Object} customConfig diff --git a/modules/brandmetricsRtdProvider.md b/modules/brandmetricsRtdProvider.md index 89ee6bb75cf..d6304f9ae12 100644 --- a/modules/brandmetricsRtdProvider.md +++ b/modules/brandmetricsRtdProvider.md @@ -16,10 +16,10 @@ Enable the Brandmetrics RTD in your Prebid configuration, using the below format pbjs.setConfig({ ..., realTimeData: { - auctionDelay: 500, // auction delay + auctionDelay: 500, dataProviders: [{ name: 'brandmetrics', - waitForIt: true // should be true if there's an `auctionDelay`, + waitForIt: true, params: { scriptId: '00000000-0000-0000-0000-000000000000', bidders: ['ozone'] @@ -29,6 +29,7 @@ pbjs.setConfig({ ... }) ``` +The scriptId- parameter is provided by brandmetrics or a brandmetrics partner. ## Parameters | Name | Type | Description | Default | @@ -38,3 +39,17 @@ pbjs.setConfig({ | params | Object | | - | | params.bidders | String[] | An array of bidders which should receive targeting keys. | `[]` | | params.scriptId | String | A script- id GUID if the brandmetrics- script should be initialized. | `undefined` | + +## Billable events +The module emits a billable event for creatives that are measured by brandmetrics and are considered in- view. + +```javascript +{ + vendor: 'brandmetrics', + type: 'creative_in_view', + measurementId: string, // UUID, brandmetrics measurement id + billingId: string, // UUID, unique billing id + auctionId: string, // Prebid auction id + transactionId: string, //Prebid transaction id +} +``` \ No newline at end of file diff --git a/modules/bridBidAdapter.js b/modules/bridBidAdapter.js new file mode 100644 index 00000000000..8e7c2f166ef --- /dev/null +++ b/modules/bridBidAdapter.js @@ -0,0 +1,223 @@ +import {createTrackPixelHtml, _each, deepAccess, getDefinedParams, parseGPTSingleSizeArrayToRtbSize} from '../src/utils.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getRefererInfo} from '../src/refererDetection.js'; + +const SOURCE = 'pbjs'; +const BIDDER_CODE = 'brid'; +const ENDPOINT_URL = 'https://pbs.prebrid.tv/openrtb2/auction'; +const GVLID = 934; +const TIME_TO_LIVE = 300; +const VIDEO_PARAMS = [ + 'api', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', + 'playbackmethod', 'protocols', 'startdelay' +]; + +export const spec = { + + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!(bid && bid.params && bid.params.placementId); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @param {BidderRequest} bidderRequest bidder request object. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(bidRequests, bidderRequest) { + const requests = []; + + _each(bidRequests, function(bid) { + const placementId = bid.params.placementId; + const bidId = bid.bidId; + let sizes = bid.sizes; + if (sizes && !Array.isArray(sizes[0])) sizes = [sizes]; + + const site = getSiteObj(); + + const postBody = { + sdk: { + source: SOURCE, + version: '$prebid.version$' + }, + id: bidderRequest.bidderRequestId, + site, + imp: [] + }; + + const imp = { + ext: { + prebid: { + storedrequest: {'id': placementId} + } + } + }; + + const video = deepAccess(bid, 'mediaTypes.video'); + if (video) { + imp.video = getDefinedParams(video, VIDEO_PARAMS); + if (video.playerSize) { + imp.video = Object.assign( + imp.video, parseGPTSingleSizeArrayToRtbSize(video.playerSize[0]) || {} + ); + } else if (video.w && video.h) { + imp.video.w = video.w; + imp.video.h = video.h; + }; + }; + + postBody.imp.push(imp); + + const gdprConsent = bidderRequest && bidderRequest.gdprConsent; + const uspConsent = bidderRequest && bidderRequest.uspConsent; + + if (gdprConsent || uspConsent) { + postBody.regs = { ext: {} }; + + if (uspConsent) { + postBody.regs.ext.us_privacy = uspConsent; + }; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies !== 'undefined') { + postBody.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; + }; + + if (typeof gdprConsent.consentString !== 'undefined') { + postBody.user = { + ext: { consent: gdprConsent.consentString } + }; + }; + }; + }; + + if (bidRequests[0].schain) { + postBody.schain = bidRequests[0].schain; + } + + const params = bid.params; + + requests.push({ + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(postBody), + options: { + withCredentials: true + }, + bidId, + params + }); + }); + + return requests; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + const response = serverResponse.body; + const bidResponses = []; + + _each(response.seatbid, (resp) => { + _each(resp.bid, (bid) => { + const requestId = bidRequest.bidId; + const params = bidRequest.params; + + const {ad, adUrl, vastUrl, vastXml} = getAd(bid); + + const bidResponse = { + requestId, + params, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.adid, + currency: response.cur, + netRevenue: false, + ttl: TIME_TO_LIVE, + meta: { + advertiserDomains: bid.adomain || [] + } + }; + + if (vastUrl || vastXml) { + bidResponse.mediaType = VIDEO; + if (vastUrl) bidResponse.vastUrl = vastUrl; + if (vastXml) bidResponse.vastXml = vastXml; + } else { + bidResponse.ad = ad; + bidResponse.adUrl = adUrl; + }; + + bidResponses.push(bidResponse); + }); + }); + + return bidResponses; + }, + +} + +/** + * Helper function to get ad + * + * @param {object} bid The bid. + * @return {object} ad object. + */ +function getAd(bid) { + let ad, adUrl, vastXml, vastUrl; + + switch (deepAccess(bid, 'ext.prebid.type')) { + case VIDEO: + if (bid.adm.substr(0, 4) === 'http') { + vastUrl = bid.adm; + } else { + vastXml = bid.adm; + }; + break; + default: + if (bid.adm && bid.nurl) { + ad = bid.adm; + ad += createTrackPixelHtml(decodeURIComponent(bid.nurl)); + } else if (bid.adm) { + ad = bid.adm; + } else if (bid.nurl) { + adUrl = bid.nurl; + }; + } + + return {ad, adUrl, vastXml, vastUrl}; +} + +/** + * Helper function to get site object + * + * @return {object} siteObj. + */ +function getSiteObj() { + const refInfo = (getRefererInfo && getRefererInfo()) || {}; + + return { + page: refInfo.page, + ref: refInfo.ref, + domain: refInfo.domain + }; +} + +registerBidder(spec); diff --git a/modules/brightcomBidAdapter.js b/modules/brightcomBidAdapter.js index c4cc5394a03..1fa1dac4e95 100644 --- a/modules/brightcomBidAdapter.js +++ b/modules/brightcomBidAdapter.js @@ -1,4 +1,17 @@ -import { getBidIdParameter, _each, isArray, getWindowTop, getUniqueIdentifierStr, deepSetValue, logError, logWarn, createTrackPixelHtml, getWindowSelf, isFn, isPlainObject } from '../src/utils.js'; +import { + _each, + isArray, + getWindowTop, + getUniqueIdentifierStr, + deepSetValue, + logError, + logWarn, + createTrackPixelHtml, + getWindowSelf, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; diff --git a/modules/brightcomSSPBidAdapter.js b/modules/brightcomSSPBidAdapter.js index b85a01c8fc7..4750881da40 100644 --- a/modules/brightcomSSPBidAdapter.js +++ b/modules/brightcomSSPBidAdapter.js @@ -1,5 +1,4 @@ import { - getBidIdParameter, isArray, getWindowTop, getUniqueIdentifierStr, @@ -9,7 +8,7 @@ import { createTrackPixelHtml, getWindowSelf, isFn, - isPlainObject, + isPlainObject, getBidIdParameter, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; diff --git a/modules/britepoolIdSystem.js b/modules/britepoolIdSystem.js index 2316fbb732d..b75fe9424b1 100644 --- a/modules/britepoolIdSystem.js +++ b/modules/britepoolIdSystem.js @@ -136,6 +136,12 @@ export const britepoolIdSubmodule = { } } return valueObj; + }, + eids: { + 'britepoolid': { + source: 'britepool.com', + atype: 3 + }, } }; diff --git a/modules/bucksenseBidAdapter.js b/modules/bucksenseBidAdapter.js index 7b6c3911ea1..42edb783d00 100644 --- a/modules/bucksenseBidAdapter.js +++ b/modules/bucksenseBidAdapter.js @@ -8,6 +8,7 @@ const URL = 'https://directo.prebidserving.com/prebidjs/'; export const spec = { code: BIDDER_CODE, + gvlid: 235, supportedMediaTypes: [BANNER], /** diff --git a/modules/cadentApertureMXBidAdapter.js b/modules/cadentApertureMXBidAdapter.js index 26e8639154c..e73564dacdb 100644 --- a/modules/cadentApertureMXBidAdapter.js +++ b/modules/cadentApertureMXBidAdapter.js @@ -1,7 +1,6 @@ import { _each, - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, isArray, isFn, isPlainObject, @@ -170,6 +169,22 @@ export const cadentAdapter = { return cadentData; }, + + getGpp: (bidRequest, cadentData) => { + if (bidRequest.gppConsent) { + const {gppString: gpp, applicableSections: gppSid} = bidRequest.gppConsent; + if (cadentData.regs) { + cadentData.regs.gpp = gpp; + cadentData.regs.gpp_sid = gppSid; + } else { + cadentData.regs = { + gpp: gpp, + gpp_sid: gppSid + } + } + } + return cadentData; + }, getSupplyChain: (bidderRequest, cadentData) => { if (bidderRequest.bids[0] && bidderRequest.bids[0].schain) { cadentData.source = { @@ -261,7 +276,7 @@ export const spec = { let isVideo = !!bid.mediaTypes.video; let data = { id: bid.bidId, - tid: bid.transactionId, + tid: bid.ortb2Imp?.ext?.tid, tagid, secure }; @@ -281,7 +296,7 @@ export const spec = { }); let cadentData = { - id: bidderRequest.auctionId, + id: bidderRequest.auctionId ?? bidderRequest.bidderRequestId, imp: cadentImps, device, site, @@ -290,6 +305,7 @@ export const spec = { }; cadentData = cadentAdapter.getGdpr(bidderRequest, Object.assign({}, cadentData)); + cadentData = cadentAdapter.getGpp(bidderRequest, Object.assign({}, cadentData)); cadentData = cadentAdapter.getSupplyChain(bidderRequest, Object.assign({}, cadentData)); if (bidderRequest && bidderRequest.uspConsent) { cadentData.us_privacy = bidderRequest.uspConsent; @@ -357,7 +373,7 @@ export const spec = { } return cadentBidResponses; }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncs = []; const consentParams = []; if (syncOptions.iframeEnabled) { @@ -373,6 +389,14 @@ export const spec = { if (uspConsent && typeof uspConsent.consentString === 'string') { consentParams.push(`usp=${uspConsent.consentString}`); } + if (gppConsent && typeof gppConsent === 'object') { + if (gppConsent.gppString && typeof gppConsent.gppString === 'string') { + consentParams.push(`gpp=${gppConsent.gppString}`); + } + if (gppConsent.applicableSections && typeof gppConsent.applicableSections === 'object') { + consentParams.push(`gpp_sid=${gppConsent.applicableSections}`); + } + } if (consentParams.length > 0) { url = url + '?' + consentParams.join('&'); } diff --git a/modules/cadentApertureMXBidAdapter.md b/modules/cadentApertureMXBidAdapter.md index 11d63134587..d924f904be4 100644 --- a/modules/cadentApertureMXBidAdapter.md +++ b/modules/cadentApertureMXBidAdapter.md @@ -3,14 +3,14 @@ ``` Module Name: Cadent Aperture MX Adapter Module Type: Bidder Adapter -Maintainer: git@emxdigital.com +Maintainer: contactaperturemx@cadent.tv ``` # Description The Cadent Aperture MX adapter provides publishers with access to the Cadent Aperture MX SSP. The adapter is GDPR compliant. Please note that the adapter supports Banner and Video (Instream & Outstream) media types. -Note: The Cadent Aperture MX adapter requires approval and implementation guidelines from the Cadent team, including existing publishers that work with Cadent. Please reach out to your account manager or prebid@emxdigital.com for more information. +Note: The Cadent Aperture MX adapter requires approval and implementation guidelines from the Cadent team, including existing publishers that work with Cadent. Please reach out to your account manager or contactaperturemx@cadent.tv for more information. The bidder code should be ```cadent_aperture_mx``` The params used by the bidder are : diff --git a/modules/categoryTranslation.js b/modules/categoryTranslation.js index ba73aa01b85..eb6cb83730a 100644 --- a/modules/categoryTranslation.js +++ b/modules/categoryTranslation.js @@ -12,7 +12,7 @@ */ import {config} from '../src/config.js'; -import {hook, setupBeforeHookFnOnce} from '../src/hook.js'; +import {hook, setupBeforeHookFnOnce, ready} from '../src/hook.js'; import {ajax} from '../src/ajax.js'; import {logError, timestamp} from '../src/utils.js'; import {addBidResponse} from '../src/auction.js'; @@ -32,7 +32,8 @@ export const registerAdserver = hook('async', function(adServer) { initTranslation(url, DEFAULT_IAB_TO_FW_MAPPING_KEY); } }, 'registerAdserver'); -registerAdserver(); + +ready.then(() => registerAdserver()); export const getAdserverCategoryHook = timedBidResponseHook('categoryTranslation', function getAdserverCategoryHook(fn, adUnitCode, bid, reject) { if (!bid) { diff --git a/modules/coinzillaBidAdapter.js b/modules/coinzillaBidAdapter.js index 15731423c49..c7d8fa5797c 100644 --- a/modules/coinzillaBidAdapter.js +++ b/modules/coinzillaBidAdapter.js @@ -77,7 +77,7 @@ export const spec = { dealId: dealId, currency: currency, netRevenue: netRevenue, - ttl: bidRequest.timeout, + ttl: response.timeout, referrer: referrer, ad: response.ad, mediaType: response.mediaType, diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index bf4079322ff..bc2c4555c54 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -41,6 +41,7 @@ export const spec = { prebidVersion: '$prebid.version$', pageUrl: bidderRequest.refererInfo.page, screen: [window.screen.width, window.screen.height].join('x'), + browserLanguage: window.navigator.language, debug: debugTurnedOn(), uid: getUid(bidderRequest, validBidRequests), optedOut: hasOptedOutOfPersonalization(), @@ -48,6 +49,7 @@ export const spec = { uspConsent: bidderRequest.uspConsent, gdprConsent: bidderRequest.gdprConsent, gppConsent: bidderRequest.gppConsent, + tdid: getTdid(bidderRequest, validBidRequests), } }; @@ -262,3 +264,11 @@ function getOffset(el) { }; } } + +function getTdid(bidderRequest, validBidRequests) { + if (hasOptedOutOfPersonalization() || !consentAllowsPpid(bidderRequest)) { + return null; + } + + return deepAccess(validBidRequests[0], 'userId.tdid') || null; +} diff --git a/modules/connatixBidAdapter.js b/modules/connatixBidAdapter.js new file mode 100644 index 00000000000..7524cd4e194 --- /dev/null +++ b/modules/connatixBidAdapter.js @@ -0,0 +1,184 @@ +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; + +import { + deepAccess, + isFn, + logError, + isArray, + formatQS +} from '../src/utils.js'; + +import { + BANNER, +} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'connatix'; +const AD_URL = 'https://capi.connatix.com/rtb/hba'; +const DEFAULT_MAX_TTL = '3600'; +const DEFAULT_CURRENCY = 'USD'; + +/* + * Get the bid floor value from the bid object, either using the getFloor function or by accessing the 'params.bidfloor' property. + * If the bid floor cannot be determined, return 0 as a fallback value. + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + gvlid: 143, + supportedMediaTypes: [BANNER], + + /* + * Validate the bid request. + * If the request is valid, Connatix is trying to obtain at least one bid. + * Otherwise, the request to the Connatix server is not made + */ + isBidRequestValid: (bid = {}) => { + const bidId = deepAccess(bid, 'bidId'); + const mediaTypes = deepAccess(bid, 'mediaTypes', {}); + const params = deepAccess(bid, 'params', {}); + const bidder = deepAccess(bid, 'bidder'); + + const banner = deepAccess(mediaTypes, BANNER, {}); + + const hasBidId = Boolean(bidId); + const isValidBidder = (bidder === BIDDER_CODE); + const isValidSize = (Boolean(banner.sizes) && isArray(mediaTypes[BANNER].sizes) && mediaTypes[BANNER].sizes.length > 0); + const hasSizes = mediaTypes[BANNER] ? isValidSize : false; + const hasRequiredBidParams = Boolean(params.placementId); + + const isValid = isValidBidder && hasBidId && hasSizes && hasRequiredBidParams; + if (!isValid) { + logError(`Invalid bid request: isValidBidder: ${isValidBidder} hasBidId: ${hasBidId}, hasSizes: ${hasSizes}, hasRequiredBidParams: ${hasRequiredBidParams}`); + } + return isValid; + }, + + /* + * Build the request payload by processing valid bid requests and extracting the necessary information. + * Determine the host and page from the bidderRequest's refferUrl, and include ccpa and gdpr consents. + * Return an object containing the request method, url, and the constructed payload. + */ + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + const bidRequests = validBidRequests.map(bid => { + const { + bidId, + mediaTypes, + params, + sizes, + } = bid; + return { + bidId, + mediaTypes, + sizes, + placementId: params.placementId, + floor: getBidFloor(bid), + }; + }); + + const requestPayload = { + ortb2: bidderRequest.ortb2, + gdprConsent: bidderRequest.gdprConsent, + uspConsent: bidderRequest.uspConsent, + refererInfo: bidderRequest.refererInfo, + bidRequests, + }; + + return { + method: 'POST', + url: AD_URL, + data: requestPayload + }; + }, + + /* + * Interpret the server response and create an array of bid responses by extracting and formatting + * relevant information such as requestId, cpm, ttl, width, height, creativeId, referrer and ad + * Returns an array of bid responses by extracting and formatting the server response + */ + interpretResponse: (serverResponse) => { + const responseBody = serverResponse.body; + const bids = responseBody.Bids; + + if (!isArray(bids)) { + return []; + } + + const referrer = responseBody.Referrer; + return bids.map(bidResponse => ({ + requestId: bidResponse.RequestId, + cpm: bidResponse.Cpm, + ttl: bidResponse.Ttl || DEFAULT_MAX_TTL, + currency: 'USD', + mediaType: BANNER, + netRevenue: true, + width: bidResponse.Width, + height: bidResponse.Height, + creativeId: bidResponse.CreativeId, + ad: bidResponse.Ad, + referrer: referrer, + })); + }, + + /* + * Determine the user sync type (either 'iframe' or 'image') based on syncOptions. + * Construct the sync URL by appending required query parameters such as gdpr, ccpa, and coppa consents. + * Return an array containing an object with the sync type and the constructed URL. + */ + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { + if (!syncOptions.iframeEnabled) { + return []; + } + + if (!serverResponses || !serverResponses.length) { + return []; + } + + const params = {}; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params['gdpr'] = Number(gdprConsent.gdprApplies); + } else { + params['gdpr'] = 0; + } + + if (typeof gdprConsent.consentString === 'string') { + params['gdpr_consent'] = encodeURIComponent(gdprConsent.consentString); + } + } + + if (typeof uspConsent === 'string') { + params['us_privacy'] = encodeURIComponent(uspConsent); + } + + const syncUrl = serverResponses[0].body.UserSyncEndpoint; + const queryParams = Object.keys(params).length > 0 ? formatQS(params) : ''; + + const url = queryParams ? `${syncUrl}?${queryParams}` : syncUrl; + return [{ + type: 'iframe', + url + }]; + } +}; + +registerBidder(spec); diff --git a/modules/connatixBidAdapter.md b/modules/connatixBidAdapter.md new file mode 100644 index 00000000000..595c294e311 --- /dev/null +++ b/modules/connatixBidAdapter.md @@ -0,0 +1,54 @@ + +# Overview + +``` +Module Name: Connatix Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid_integration@connatix.com +``` + +# Description +Connects to Connatix demand source to fetch bids. +Please use ```connatix``` as the bidder code. + +# Configuration +Connatix requires that ```iframe``` is used for user syncing. + +Example configuration: +``` +pbjs.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: '*', // represents all bidders + filter: 'include' + } + } + } +}); +``` + +# Test Parameters +``` +var adUnits = [ + { + code: '1', + mediaTypes: { + banner: { + sizes: [[640, 480], [320, 180]], + }, + }, + bids: [ + { + bidder: 'connatix', + params: { + placementId: 'e4984e88-9ff4-45a3-8b9d-33aabcad634e', // required + bidfloor: 2.5, // optional + }, + }, + // Add more bidders and their parameters as needed + ], + }, + // Define more ad units here if necessary +]; +``` \ No newline at end of file diff --git a/modules/connectIdSystem.js b/modules/connectIdSystem.js index 1caff49199d..35a77a9d72d 100644 --- a/modules/connectIdSystem.js +++ b/modules/connectIdSystem.js @@ -10,7 +10,7 @@ import {submodule} from '../src/hook.js'; import {includes} from '../src/polyfill.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {getStorageManager} from '../src/storageManager.js'; -import {formatQS, isPlainObject, logError, parseUrl} from '../src/utils.js'; +import {formatQS, isNumber, isPlainObject, logError, parseUrl} from '../src/utils.js'; import {uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; @@ -26,6 +26,16 @@ const PLACEHOLDER = '__PIXEL_ID__'; const UPS_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`; const OVERRIDE_OPT_OUT_KEY = 'connectIdOptOut'; const INPUT_PARAM_KEYS = ['pixelId', 'he', 'puid']; +const O_AND_O_DOMAINS = [ + 'yahoo.com', + 'aol.com', + 'aol.ca', + 'aol.de', + 'aol.co.uk', + 'engadget.com', + 'techcrunch.com', + 'autoblog.com', +]; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @@ -104,9 +114,11 @@ function syncLocalStorageToCookie() { } function isStale(storedIdData) { - if (isPlainObject(storedIdData) && storedIdData.lastSynced && - (storedIdData.lastSynced + VALID_ID_DURATION) <= Date.now()) { + if (isOAndOTraffic()) { return true; + } else if (isPlainObject(storedIdData) && storedIdData.lastSynced) { + const validTTL = storedIdData.ttl || VALID_ID_DURATION; + return storedIdData.lastSynced + validTTL <= Date.now(); } return false; } @@ -127,6 +139,17 @@ function getSiteHostname() { return pageInfo.hostname; } +function isOAndOTraffic() { + let referer = getRefererInfo().ref; + + if (referer) { + referer = parseUrl(referer).hostname; + const subDomains = referer.split('.'); + referer = subDomains.slice(subDomains.length - 2, subDomains.length).join('.'); + } + return O_AND_O_DOMAINS.indexOf(referer) >= 0; +} + /** @type {Submodule} */ export const connectIdSubmodule = { /** @@ -238,6 +261,13 @@ export const connectIdSubmodule = { responseObj.puid = params.puid || responseObj.puid; responseObj.lastSynced = Date.now(); responseObj.lastUsed = Date.now(); + if (isNumber(responseObj.ttl)) { + let validTTLMiliseconds = responseObj.ttl * 60 * 60 * 1000; + if (validTTLMiliseconds > VALID_ID_DURATION) { + validTTLMiliseconds = VALID_ID_DURATION; + } + responseObj.ttl = validTTLMiliseconds; + } storeObject(responseObj); } else { logError(`${MODULE_NAME} module: UPS response returned an invalid payload ${response}`); @@ -294,6 +324,12 @@ export const connectIdSubmodule = { */ getAjaxFn() { return ajax; + }, + eids: { + 'connectId': { + source: 'yahoo.com', + atype: 3 + }, } }; diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js index d5665b318be..b40ef30f6bc 100644 --- a/modules/connectadBidAdapter.js +++ b/modules/connectadBidAdapter.js @@ -1,7 +1,9 @@ -import { deepSetValue, convertTypes, tryAppendQueryString, logWarn } from '../src/utils.js'; +import { deepSetValue, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' import {config} from '../src/config.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'connectad'; const BIDDER_CODE_ALIAS = 'connectadrealtime'; const ENDPOINT_URL = 'https://i.connectad.io/api/v2'; diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 6c22b3b9da4..05447a890cb 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -12,6 +12,7 @@ import {timedAuctionHook} from '../src/utils/perfMetrics.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {enrichFPD} from '../src/fpd/enrichment.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {cmpClient} from '../libraries/cmp/cmpClient.js'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; @@ -48,36 +49,6 @@ function lookupStaticConsentData({onSuccess, onError}) { * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) */ function lookupIabConsent({onSuccess, onError, onEvent}) { - function findCMP() { - let f = window; - let cmpFrame; - let cmpFunction; - while (true) { - try { - if (typeof f.__tcfapi === 'function') { - cmpFunction = f.__tcfapi; - cmpFrame = f; - break; - } - } catch (e) { } - - // need separate try/catch blocks due to the exception errors thrown when trying to check for a frame that doesn't exist in 3rd party env - try { - if (f.frames['__tcfapiLocator']) { - cmpFrame = f; - break; - } - } catch (e) { } - - if (f === window.top) break; - f = f.parent; - } - return { - cmpFrame, - cmpFunction - }; - } - function cmpResponseCallback(tcfData, success) { logInfo('Received a response from CMP', tcfData); if (success) { @@ -90,70 +61,25 @@ function lookupIabConsent({onSuccess, onError, onEvent}) { } } - const cmpCallbacks = {}; - const { cmpFrame, cmpFunction } = findCMP(); + const cmp = cmpClient({ + apiName: '__tcfapi', + apiVersion: CMP_VERSION, + apiArgs: ['command', 'version', 'callback', 'parameter'], + }); - if (!cmpFrame) { + if (!cmp) { return onError('TCF2 CMP not found.'); } - // to collect the consent information from the user, we perform two calls to the CMP in parallel: - // first to collect the user's consent choices represented in an encoded string (via getConsentData) - // second to collect the user's full unparsed consent information (via getVendorConsents) - - // the following code also determines where the CMP is located and uses the proper workflow to communicate with it: - // check to see if CMP is found on the same window level as prebid and call it directly if so - // check to see if prebid is in a safeframe (with CMP support) - // else assume prebid may be inside an iframe and use the IAB CMP locator code to see if CMP's located in a higher parent window. this works in cross domain iframes - // if the CMP is not found, the iframe function will call the cmpError exit callback to abort the rest of the CMP workflow - - if (typeof cmpFunction === 'function') { + if (cmp.isDirect) { logInfo('Detected CMP API is directly accessible, calling it now...'); - cmpFunction('addEventListener', CMP_VERSION, cmpResponseCallback); } else { logInfo('Detected CMP is outside the current iframe where Prebid.js is located, calling it now...'); - callCmpWhileInIframe('addEventListener', cmpFrame, cmpResponseCallback); } - function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) { - const apiName = '__tcfapi'; - - const callName = `${apiName}Call`; - - /* Setup up a __cmp function to do the postMessage and stash the callback. - This function behaves (from the caller's perspective identicially to the in-frame __cmp call */ - window[apiName] = function (cmd, cmpVersion, callback, arg) { - const callId = Math.random() + ''; - const msg = { - [callName]: { - command: cmd, - version: cmpVersion, - parameter: arg, - callId: callId - } - }; - - cmpCallbacks[callId] = callback; - cmpFrame.postMessage(msg, '*'); - } - - /** when we get the return message, call the stashed callback */ - window.addEventListener('message', readPostMessageResponse, false); - - // call CMP - window[apiName](commandName, CMP_VERSION, moduleCallback); - - function readPostMessageResponse(event) { - const cmpDataPkgName = `${apiName}Return`; - const json = (typeof event.data === 'string' && includes(event.data, cmpDataPkgName)) ? JSON.parse(event.data) : event.data; - if (json[cmpDataPkgName] && json[cmpDataPkgName].callId) { - const payload = json[cmpDataPkgName]; - // TODO - clean up this logic (move listeners?); we have duplicate messages responses because 2 eventlisteners are active from the 2 cmp requests running in parallel - if (cmpCallbacks.hasOwnProperty(payload.callId)) { - cmpCallbacks[payload.callId](payload.returnValue, payload.success); - } - } - } - } + cmp({ + command: 'addEventListener', + callback: cmpResponseCallback + }) } /** diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js index 3b0e1c25c6a..8160ee2378c 100644 --- a/modules/consentManagementGpp.js +++ b/modules/consentManagementGpp.js @@ -4,180 +4,314 @@ * and make it available for any GPP supported adapters to read/pass this information to * their system and for various other features/modules in Prebid.js. */ -import {deepSetValue, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {deepSetValue, isEmpty, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import {gppDataHandler} from '../src/adapterManager.js'; -import {includes} from '../src/polyfill.js'; import {timedAuctionHook} from '../src/utils/perfMetrics.js'; -import { enrichFPD } from '../src/fpd/enrichment.js'; +import {enrichFPD} from '../src/fpd/enrichment.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {cmpClient, MODE_CALLBACK, MODE_MIXED, MODE_RETURN} from '../libraries/cmp/cmpClient.js'; +import {GreedyPromise} from '../src/utils/promise.js'; +import {buildActivityParams} from '../src/activities/params.js'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; -const CMP_VERSION = 1; export let userCMP; export let consentTimeout; -export let staticConsentData; +let staticConsentData; let consentData; let addedConsentHook = false; -// add new CMPs here, with their dedicated lookup function -const cmpCallMap = { - 'iab': lookupIabConsent, - 'static': lookupStaticConsentData -}; - -/** - * This function checks the state of the IAB gppData's applicableSection field (to ensure it's populated and has a valid value). - * section === 0 represents a CMP's default value when CMP is loading, it shoud not be used a real user's section. - * - * TODO --- The initial version of the GPP CMP API spec used this naming convention, but it was later changed as an update to the spec. - * CMPs should adjust their logic to use the new format (applicableSecctions), but that may not be the case with the initial release. - * Added support just in case for this transition period, can likely be removed at a later date... - * @param gppData represents the IAB gppData object - * @returns true|false - */ -function checkApplicableSectionIsReady(gppData) { - return gppData && Array.isArray(gppData.applicableSection) && gppData.applicableSection.length > 0 && gppData.applicableSection[0] !== 0; +function pipeCallbacks(fn, {onSuccess, onError}) { + new GreedyPromise((resolve) => resolve(fn())).then(onSuccess, (err) => { + if (err instanceof GPPError) { + onError(err.message, ...err.args); + } else { + onError(`GPP error:`, err); + } + }); } -/** - * This function checks the state of the IAB gppData's applicableSections field (to ensure it's populated and has a valid value). - * section === 0 represents a CMP's default value when CMP is loading, it shoud not be used a real user's section. - * @param gppData represents the IAB gppData object - * @returns true|false - */ -function checkApplicableSectionsIsReady(gppData) { - return gppData && Array.isArray(gppData.applicableSections) && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== 0; +function lookupStaticConsentData(callbacks) { + return pipeCallbacks(() => processCmpData(staticConsentData), callbacks); } -/** - * This function reads the consent string from the config to obtain the consent information of the user. - * @param {function({})} onSuccess acts as a success callback when the value is read from config; pass along consentObject from CMP - */ -function lookupStaticConsentData({onSuccess, onError}) { - processCmpData(staticConsentData, {onSuccess, onError}); +const GPP_10 = '1.0'; +const GPP_11 = '1.1'; + +class GPPError { + constructor(message, arg) { + this.message = message; + this.args = arg == null ? [] : [arg]; + } } -/** - * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. - * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function - * based on the appropriate result. - * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP - * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) - */ -function lookupIabConsent({onSuccess, onError}) { - const cmpApiName = '__gpp'; - const cmpCallbacks = {}; - let registeredPostMessageResponseListener = false; - - function findCMP() { - let f = window; - let cmpFrame; - let cmpDirectAccess = false; - while (true) { - try { - if (typeof f[cmpApiName] === 'function') { - cmpFrame = f; - cmpDirectAccess = true; - break; - } - } catch (e) {} +export class GPPClient { + static CLIENTS = {}; - // need separate try/catch blocks due to the exception errors thrown when trying to check for a frame that doesn't exist in 3rd party env - try { - if (f.frames['__gppLocator']) { - cmpFrame = f; - break; - } - } catch (e) {} + static register(apiVersion, defaultVersion = false) { + this.apiVersion = apiVersion; + this.CLIENTS[apiVersion] = this; + if (defaultVersion) { + this.CLIENTS.default = this; + } + } - if (f === window.top) break; - f = f.parent; + static INST; + + /** + * Ping the CMP to set up an appropriate client for it, and initialize it. + * + * @param mkCmp + * @returns {Promise<[GPPClient,Promise<{}>]>} a promise to two objects: + * - a GPPClient that talks the best GPP dialect we know for the CMP's version; + * - a promise to GPP data. + */ + static init(mkCmp = cmpClient) { + if (this.INST == null) { + this.INST = this.ping(mkCmp).catch(e => { + this.INST = null; + throw e; + }); } + return this.INST.then(([client, pingData]) => [ + client, + client.initialized ? client.refresh() : client.init(pingData) + ]); + } - return { - cmpFrame, - cmpDirectAccess + /** + * Ping the CMP to determine its version and set up a client appropriate for it. + * + * @param mkCmp + * @returns {Promise<[GPPClient, {}]>} a promise to two objects: + * - a GPPClient that talks the best GPP dialect we know for the CMP's version; + * - the result from pinging the CMP. + */ + static ping(mkCmp = cmpClient) { + const cmpOptions = { + apiName: '__gpp', + apiArgs: ['command', 'callback', 'parameter'], // do not pass version - not clear what it's for (or what we should use) }; - } - const {cmpFrame, cmpDirectAccess} = findCMP(); + // in 1.0, 'ping' should return pingData but ignore callback; + // in 1.1 it should not return anything but run the callback + // the following looks for either - but once the version is known, produce a client that knows whether the + // rest of the interactions should pick return values or pass callbacks - if (!cmpFrame) { - return onError('GPP CMP not found.'); + const probe = mkCmp({...cmpOptions, mode: MODE_RETURN}); + return new GreedyPromise((resolve, reject) => { + if (probe == null) { + reject(new GPPError('GPP CMP not found')); + return; + } + let done = false; // some CMPs do both return value and callbacks - avoid repeating log messages + const pong = (result, success) => { + if (done) return; + if (success != null && !success) { + reject(result); + return; + } + if (result == null) return; + done = true; + const cmpVersion = result?.gppVersion; + const Client = this.getClient(cmpVersion); + if (cmpVersion !== Client.apiVersion) { + logWarn(`Unrecognized GPP CMP version: ${cmpVersion}. Continuing using GPP API version ${Client}...`); + } else { + logInfo(`Using GPP version ${cmpVersion}`); + } + const mode = Client.apiVersion === GPP_10 ? MODE_MIXED : MODE_CALLBACK; + const client = new Client( + cmpVersion, + mkCmp({...cmpOptions, mode}) + ); + resolve([client, result]); + }; + + probe({ + command: 'ping', + callback: pong + }).then((res) => pong(res, true), reject); + }).finally(() => { + probe && probe.close(); + }); } - const invokeCMP = (cmpDirectAccess) ? invokeCMPDirect : invokeCMPFrame; + static getClient(cmpVersion) { + return this.CLIENTS.hasOwnProperty(cmpVersion) ? this.CLIENTS[cmpVersion] : this.CLIENTS.default; + } - function invokeCMPDirect({command, callback, parameter, version = CMP_VERSION}, resultCb) { - if (typeof resultCb === 'function') { - resultCb(cmpFrame[cmpApiName](command, callback, parameter, version)); - } else { - cmpFrame[cmpApiName](command, callback, parameter, version); - } + #resolve; + #reject; + #pending = []; + + initialized = false; + + constructor(cmpVersion, cmp) { + this.apiVersion = this.constructor.apiVersion; + this.cmpVersion = cmp; + this.cmp = cmp; + [this.#resolve, this.#reject] = [0, 1].map(slot => (result) => { + while (this.#pending.length) { + this.#pending.pop()[slot](result); + } + }); } - function invokeCMPFrame({command, callback, parameter, version = CMP_VERSION}, resultCb) { - const callName = `${cmpApiName}Call`; - if (!registeredPostMessageResponseListener) { - // when we get the return message, call the stashed callback; - window.addEventListener('message', readPostMessageResponse, false); - registeredPostMessageResponseListener = true; + /** + * initialize this client - update consent data if already available, + * and set up event listeners to also update on CMP changes + * + * @param pingData + * @returns {Promise<{}>} a promise to GPP consent data + */ + init(pingData) { + const ready = this.updateWhenReady(pingData); + if (!this.initialized) { + this.initialized = true; + this.cmp({ + command: 'addEventListener', + callback: (event, success) => { + if (success != null && !success) { + this.#reject(new GPPError('Received error response from CMP', event)); + } else if (event?.pingData?.cmpStatus === 'error') { + this.#reject(new GPPError('CMP status is "error"; please check CMP setup', event)); + } else if (this.isCMPReady(event?.pingData || {}) && this.events.includes(event?.eventName)) { + this.#resolve(this.updateConsent(event.pingData)); + } + } + }); } + return ready; + } - // call CMP via postMessage - const callId = Math.random().toString(); - const msg = { - [callName]: { - command: command, - parameter, - version, - callId: callId + refresh() { + return this.cmp({command: 'ping'}).then(this.updateWhenReady.bind(this)); + } + + /** + * Retrieve and store GPP consent data. + * + * @param pingData + * @returns {Promise<{}>} a promise to GPP consent data + */ + updateConsent(pingData) { + return this.getGPPData(pingData).then((data) => { + if (data == null || isEmpty(data)) { + throw new GPPError('Received empty response from CMP', data); } - }; + return processCmpData(data); + }).then((data) => { + logInfo('Retrieved GPP consent from CMP:', data); + return data; + }); + } - // TODO? - add logic to check if random was already used in the same session, and roll another if so? - cmpCallbacks[callId] = (typeof callback === 'function') ? callback : resultCb; - cmpFrame.postMessage(msg, '*'); + /** + * Return a promise to GPP consent data, to be retrieved the next time the CMP signals it's ready. + * + * @returns {Promise<{}>} + */ + nextUpdate() { + return new GreedyPromise((resolve, reject) => { + this.#pending.push([resolve, reject]); + }); + } - function readPostMessageResponse(event) { - const cmpDataPkgName = `${cmpApiName}Return`; - const json = (typeof event.data === 'string' && event.data.includes(cmpDataPkgName)) ? JSON.parse(event.data) : event.data; - if (json[cmpDataPkgName] && json[cmpDataPkgName].callId) { - const payload = json[cmpDataPkgName]; + /** + * Return a promise to GPP consent data, to be retrieved immediately if the CMP is ready according to `pingData`, + * or as soon as it signals that it's ready otherwise. + * + * @param pingData + * @returns {Promise<{}>} + */ + updateWhenReady(pingData) { + return this.isCMPReady(pingData) ? this.updateConsent(pingData) : this.nextUpdate(); + } +} - if (cmpCallbacks.hasOwnProperty(payload.callId)) { - cmpCallbacks[payload.callId](payload.returnValue); - } - } - } +// eslint-disable-next-line no-unused-vars +class GPP10Client extends GPPClient { + static { + super.register(GPP_10); } - const startupMsg = (cmpDirectAccess) ? 'Detected GPP CMP API is directly accessible, calling it now...' - : 'Detected GPP CMP is outside the current iframe where Prebid.js is located, calling it now...'; - logInfo(startupMsg); - - invokeCMP({ - command: 'addEventListener', - callback: function (evt) { - if (evt) { - logInfo(`Received a ${(cmpDirectAccess ? 'direct' : 'postmsg')} response from GPP CMP for event`, evt); - if (evt.eventName === 'sectionChange' || evt.pingData.cmpStatus === 'loaded') { - invokeCMP({command: 'getGPPData'}, function (gppData) { - logInfo(`Received a ${cmpDirectAccess ? 'direct' : 'postmsg'} response from GPP CMP for getGPPData`, gppData); - processCmpData(gppData, {onSuccess, onError}); - }); - } else if (evt.pingData.cmpStatus === 'error') { - onError('CMP returned with a cmpStatus:error response. Please check CMP setup.'); - } - } - } - }); + events = ['sectionChange', 'cmpStatus']; + + isCMPReady(pingData) { + return pingData.cmpStatus === 'loaded'; + } + + getGPPData(pingData) { + const parsedSections = GreedyPromise.all( + (pingData.supportedAPIs || pingData.apiSupport || []).map((api) => this.cmp({ + command: 'getSection', + parameter: api + }).catch(err => { + logWarn(`Could not retrieve GPP section '${api}'`, err); + }).then((section) => [api, section])) + ).then(sections => { + // parse single section object into [core, gpc] to uniformize with 1.1 parsedSections + return Object.fromEntries( + sections.filter(([_, val]) => val != null) + .map(([api, section]) => { + const subsections = [ + Object.fromEntries(Object.entries(section).filter(([k]) => k !== 'Gpc')) + ]; + if (section.Gpc != null) { + subsections.push({ + SubsectionType: 1, + Gpc: section.Gpc + }); + } + return [api, subsections]; + }) + ); + }); + return GreedyPromise.all([ + this.cmp({command: 'getGPPData'}), + parsedSections + ]).then(([gppData, parsedSections]) => Object.assign({}, gppData, {parsedSections})); + } +} + +// eslint-disable-next-line no-unused-vars +class GPP11Client extends GPPClient { + static { + super.register(GPP_11, true); + } + + events = ['sectionChange', 'signalStatus']; + + isCMPReady(pingData) { + return pingData.signalStatus === 'ready'; + } + + getGPPData(pingData) { + return GreedyPromise.resolve(pingData); + } } +/** + * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. + * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function + * based on the appropriate result. + * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP + * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) + */ +export function lookupIabConsent({onSuccess, onError}, mkCmp = cmpClient) { + pipeCallbacks(() => GPPClient.init(mkCmp).then(([client, gppDataPm]) => gppDataPm), {onSuccess, onError}); +} + +// add new CMPs here, with their dedicated lookup function +const cmpCallMap = { + 'iab': lookupIabConsent, + 'static': lookupStaticConsentData +}; + /** * Look up consent data and store it in the `consentData` global as well as `adapterManager.js`' gdprDataHandler. * @@ -199,7 +333,7 @@ function loadConsentData(cb) { } } - if (!includes(Object.keys(cmpCallMap), userCMP)) { + if (!cmpCallMap.hasOwnProperty(userCMP)) { done(null, false, `GPP CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); return; } @@ -209,19 +343,19 @@ function loadConsentData(cb) { onError: function (msg, ...extraArgs) { done(null, true, msg, ...extraArgs); } - } + }; cmpCallMap[userCMP](callbacks); if (!isDone) { const onTimeout = () => { const continueToAuction = (data) => { done(data, false, 'GPP CMP did not load, continuing auction...'); - } - processCmpData(consentData, { + }; + pipeCallbacks(() => processCmpData(consentData), { onSuccess: continueToAuction, - onError: () => continueToAuction(storeConsentData(undefined)) - }) - } + onError: () => continueToAuction(storeConsentData()) + }); + }; if (consentTimeout === 0) { onTimeout(); } else { @@ -276,43 +410,30 @@ export const requestBidsHook = timedAuctionHook('gpp', function requestBidsHook( }); }); -/** - * This function checks the consent data provided by CMP to ensure it's in an expected state. - * If it's bad, we call `onError` - * If it's good, then we store the value and call `onSuccess` - */ -function processCmpData(consentObject, {onSuccess, onError}) { - function checkData() { - const gppString = consentObject && consentObject.gppString; - const gppSection = (checkApplicableSectionsIsReady(consentObject)) ? consentObject.applicableSections - : (checkApplicableSectionIsReady(consentObject)) ? consentObject.applicableSection : []; - - return !!( - (!Array.isArray(gppSection)) || - (Array.isArray(gppSection) && (!gppString || !isStr(gppString))) - ); - } - - if (checkData()) { - onError(`CMP returned unexpected value during lookup process.`, consentObject); - } else { - onSuccess(storeConsentData(consentObject)); +function processCmpData(consentData) { + if ( + (consentData?.applicableSections != null && !Array.isArray(consentData.applicableSections)) || + (consentData?.gppString != null && !isStr(consentData.gppString)) || + (consentData?.parsedSections != null && !isPlainObject(consentData.parsedSections)) + ) { + throw new GPPError('CMP returned unexpected value during lookup process.', consentData); } + return storeConsentData(consentData); } /** * Stores CMP data locally in module to make information available in adaptermanager.js for later in the auction - * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) + * @param {{}} gppData the result of calling a CMP's `getGPPData` (or equivalent) + * @param {{}} sectionData map from GPP section name to the result of calling a CMP's `getSection` (or equivalent) */ -function storeConsentData(cmpConsentObject) { +export function storeConsentData(gppData = {}) { consentData = { - gppString: (cmpConsentObject) ? cmpConsentObject.gppString : undefined, - - fullGppData: (cmpConsentObject) || undefined, + gppString: gppData?.gppString, + applicableSections: gppData?.applicableSections || [], + parsedSections: gppData?.parsedSections || {}, + gppData: gppData }; - consentData.applicableSections = (checkApplicableSectionsIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSections - : (checkApplicableSectionIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSection : []; - consentData.apiVersion = CMP_VERSION; + gppDataHandler.setConsentData(gppData); return consentData; } @@ -324,6 +445,7 @@ export function resetConsentData() { userCMP = undefined; consentTimeout = undefined; gppDataHandler.reset(); + GPPClient.INST = null; } /** @@ -364,11 +486,15 @@ export function setConsentConfig(config) { if (!addedConsentHook) { getGlobal().requestBids.before(requestBidsHook, 50); + buildActivityParams.before((next, params) => { + return next(Object.assign({gppConsent: gppDataHandler.getConsentData()}, params)); + }); } addedConsentHook = true; gppDataHandler.enable(); loadConsentData(); // immediately look up consent data to make it available without requiring an auction } + config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); export function enrichFPDHook(next, fpd) { diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index fcc13152aa9..1218c3724f4 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -4,12 +4,13 @@ * information and make it available for any USP (CCPA) supported adapters to * read/pass this information to their system. */ -import {deepSetValue, isFn, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {deepSetValue, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {uspDataHandler} from '../src/adapterManager.js'; import {timedAuctionHook} from '../src/utils/perfMetrics.js'; import {getHook} from '../src/hook.js'; import {enrichFPD} from '../src/fpd/enrichment.js'; +import {cmpClient} from '../libraries/cmp/cmpClient.js'; const DEFAULT_CONSENT_API = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 50; @@ -41,35 +42,6 @@ function lookupStaticConsentData({onSuccess, onError}) { * based on the appropriate result. */ function lookupUspConsent({onSuccess, onError}) { - function findUsp() { - let f = window; - let uspapiFrame; - let uspapiFunction; - - while (true) { - try { - if (typeof f.__uspapi === 'function') { - uspapiFunction = f.__uspapi; - uspapiFrame = f; - break; - } - } catch (e) {} - - try { - if (f.frames['__uspapiLocator']) { - uspapiFrame = f; - break; - } - } catch (e) {} - if (f === window.top) break; - f = f.parent; - } - return { - uspapiFrame, - uspapiFunction, - }; - } - function handleUspApiResponseCallbacks() { const uspResponse = {}; @@ -92,86 +64,36 @@ function lookupUspConsent({onSuccess, onError}) { } let callbackHandler = handleUspApiResponseCallbacks(); - let uspapiCallbacks = {}; - let { uspapiFrame, uspapiFunction } = findUsp(); + const cmp = cmpClient({ + apiName: '__uspapi', + apiVersion: USPAPI_VERSION, + apiArgs: ['command', 'version', 'callback'], + }); - if (!uspapiFrame) { + if (!cmp) { return onError('USP CMP not found.'); } - function registerDataDelHandler(invoker, arg2) { - try { - invoker('registerDeletion', arg2, adapterManager.callDataDeletionRequest); - } catch (e) { - logError('Error invoking CMP `registerDeletion`:', e); - } - } - - // to collect the consent information from the user, we perform a call to USPAPI - // to collect the user's consent choices represented as a string (via getUSPData) - - // the following code also determines where the USPAPI is located and uses the proper workflow to communicate with it: - // - use the USPAPI locator code to see if USP's located in the current window or an ancestor window. - // - else assume prebid is in an iframe, and use the locator to see if the CMP is located in a higher parent window. This works in cross domain iframes. - // - if USPAPI is not found, the iframe function will call the uspError exit callback to abort the rest of the USPAPI workflow - - if (isFn(uspapiFunction)) { + if (cmp.isDirect) { logInfo('Detected USP CMP is directly accessible, calling it now...'); - uspapiFunction( - 'getUSPData', - USPAPI_VERSION, - callbackHandler.consentDataCallback - ); - registerDataDelHandler(uspapiFunction, USPAPI_VERSION); } else { logInfo( 'Detected USP CMP is outside the current iframe where Prebid.js is located, calling it now...' ); - callUspApiWhileInIframe( - 'getUSPData', - uspapiFrame, - callbackHandler.consentDataCallback - ); - registerDataDelHandler(callUspApiWhileInIframe, uspapiFrame); } - let listening = false; - - function callUspApiWhileInIframe(commandName, uspapiFrame, moduleCallback) { - function callUsp(cmd, ver, callback) { - let callId = Math.random() + ''; - let msg = { - __uspapiCall: { - command: cmd, - version: ver, - callId: callId, - }, - }; - - uspapiCallbacks[callId] = callback; - uspapiFrame.postMessage(msg, '*'); - }; - - /** when we get the return message, call the stashed callback */ - if (!listening) { - window.addEventListener('message', readPostMessageResponse, false); - listening = true; - } - - // call uspapi - callUsp(commandName, USPAPI_VERSION, moduleCallback); + cmp({ + command: 'getUSPData', + callback: callbackHandler.consentDataCallback + }); - function readPostMessageResponse(event) { - const res = event && event.data && event.data.__uspapiReturn; - if (res && res.callId) { - if (uspapiCallbacks.hasOwnProperty(res.callId)) { - uspapiCallbacks[res.callId](res.returnValue, res.success); - delete uspapiCallbacks[res.callId]; - } - } - } - } + cmp({ + command: 'registerDeletion', + callback: (res, success) => (success == null || success) && adapterManager.callDataDeletionRequest(res) + }).catch(e => { + logError('Error invoking CMP `registerDeletion`:', e); + }); } /** diff --git a/modules/consumableBidAdapter.js b/modules/consumableBidAdapter.js index 4e2a92fb594..696549a67dc 100644 --- a/modules/consumableBidAdapter.js +++ b/modules/consumableBidAdapter.js @@ -65,6 +65,11 @@ export const spec = { }; } + if (bidderRequest && bidderRequest.gppConsent && bidderRequest.gppConsent.gppString) { + data.gpp = bidderRequest.gppConsent.gppString; + data.gpp_sid = bidderRequest.gppConsent.applicableSections; + } + if (bidderRequest && bidderRequest.uspConsent) { data.ccpa = bidderRequest.uspConsent; } @@ -180,20 +185,26 @@ export const spec = { return bidResponses; }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { let syncUrl = 'https://sync.serverbid.com/ss/' + siteId + '.html'; if (syncOptions.iframeEnabled) { if (gdprConsent && gdprConsent.consentString) { if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl = appendUrlParam(syncUrl, `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`); + syncUrl = appendUrlParam(syncUrl, `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${encodeURIComponent(gdprConsent.consentString) || ''}`); } else { - syncUrl = appendUrlParam(syncUrl, `gdpr=0&gdpr_consent=${gdprConsent.consentString}`); + syncUrl = appendUrlParam(syncUrl, `gdpr=0&gdpr_consent=${encodeURIComponent(gdprConsent.consentString) || ''}`); + } + } + if (gppConsent && gppConsent.gppString) { + syncUrl = appendUrlParam(syncUrl, `gpp=${encodeURIComponent(gppConsent.gppString)}`); + if (gppConsent.applicableSections && gppConsent.applicableSections.length > 0) { + syncUrl = appendUrlParam(syncUrl, `gpp_sid=${encodeURIComponent(gppConsent.applicableSections.join(','))}`); } } - if (uspConsent && uspConsent.consentString) { - syncUrl = appendUrlParam(syncUrl, `us_privacy=${uspConsent.consentString}`); + if (uspConsent) { + syncUrl = appendUrlParam(syncUrl, `us_privacy=${encodeURIComponent(uspConsent)}`); } if (!serverResponses || serverResponses.length === 0 || !serverResponses[0].body.bdr || serverResponses[0].body.bdr !== 'cx') { diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index fd436e51461..bef65a43616 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -3,22 +3,21 @@ import { isStr, deepAccess, isArray, - getBidIdParameter, deepSetValue, isEmpty, _each, - convertTypes, parseUrl, mergeDeep, buildUrl, _map, logError, isFn, - isPlainObject, + isPlainObject, getBidIdParameter, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; // Maintainer: mediapsr@epsilon.com diff --git a/modules/cpmstarBidAdapter.js b/modules/cpmstarBidAdapter.js index 9e237ef2558..e076fb4b0bb 100755 --- a/modules/cpmstarBidAdapter.js +++ b/modules/cpmstarBidAdapter.js @@ -1,8 +1,8 @@ - import * as utils from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {getBidIdParameter} from '../src/utils.js'; const BIDDER_CODE = 'cpmstar'; @@ -49,13 +49,13 @@ export const spec = { var bidRequest = validBidRequests[i]; var referer = bidderRequest.refererInfo.page ? bidderRequest.refererInfo.page : bidderRequest.refererInfo.domain; referer = encodeURIComponent(referer); - var e = utils.getBidIdParameter('endpoint', bidRequest.params); + var e = getBidIdParameter('endpoint', bidRequest.params); var ENDPOINT = e == 'dev' ? ENDPOINT_DEV : e == 'staging' ? ENDPOINT_STAGING : ENDPOINT_PRODUCTION; var mediaType = spec.getMediaType(bidRequest); var playerSize = spec.getPlayerSize(bidRequest); var videoArgs = '&fv=0' + (playerSize ? ('&w=' + playerSize[0] + '&h=' + playerSize[1]) : ''); var url = ENDPOINT + '?media=' + mediaType + (mediaType == VIDEO ? videoArgs : '') + - '&json=c_b&mv=1&poolid=' + utils.getBidIdParameter('placementId', bidRequest.params) + + '&json=c_b&mv=1&poolid=' + getBidIdParameter('placementId', bidRequest.params) + '&reachedTop=' + encodeURIComponent(bidderRequest.refererInfo.reachedTop) + '&requestid=' + bidRequest.bidId + '&referer=' + encodeURIComponent(referer); diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 8f7821173c1..a2a054d7659 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -1,4 +1,4 @@ -import {convertCamelToUnderscore, convertTypes, getBidRequest, logError} from '../src/utils.js'; +import {getBidRequest, logError} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {auctionManager} from '../src/auctionManager.js'; @@ -7,7 +7,9 @@ import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'craft'; const URL_BASE = 'https://gacraft.jp/prebid-v3'; diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 993346df849..7c9411ccc2d 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -3,19 +3,22 @@ import { loadExternalScript } from '../src/adloader.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { find } from '../src/polyfill.js'; import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; // ref#2 import { getStorageManager } from '../src/storageManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; import { hasPurpose1Consent } from '../src/utils/gpdr.js'; import { Renderer } from '../src/Renderer.js'; import { OUTSTREAM } from '../src/video.js'; +import { ajax } from '../src/ajax.js'; const GVLID = 91; export const ADAPTER_VERSION = 36; const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; const PROFILE_ID_INLINE = 207; +const FLEDGE_SELLER_DOMAIN = 'https://grid-mercury.criteo.com'; +const FLEDGE_SELLER_TIMEOUT = 500; +const FLEDGE_DECISION_LOGIC_URL = 'https://grid-mercury.criteo.com/fledge/decision'; export const PROFILE_ID_PUBLISHERTAG = 185; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const LOG_PREFIX = 'Criteo: '; @@ -28,7 +31,7 @@ const LOG_PREFIX = 'Criteo: '; Unminified source code can be found in the privately shared repo: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js */ const FAST_BID_VERSION_PLACEHOLDER = '%FAST_BID_VERSION%'; -export const FAST_BID_VERSION_CURRENT = 136; +export const FAST_BID_VERSION_CURRENT = 139; const FAST_BID_VERSION_LATEST = 'latest'; const FAST_BID_VERSION_NONE = 'none'; const PUBLISHER_TAG_URL_TEMPLATE = 'https://static.criteo.net/js/ld/publishertag.prebid' + FAST_BID_VERSION_PLACEHOLDER + '.js'; @@ -201,7 +204,7 @@ export const spec = { /** * @param {*} response * @param {ServerRequest} request - * @return {Bid[]} + * @return {Bid[] | {bids: Bid[], fledgeAuctionConfigs: object[]}} */ interpretResponse: (response, request) => { const body = response.body || response; @@ -215,57 +218,99 @@ export const spec = { } const bids = []; + const fledgeAuctionConfigs = []; if (body && body.slots && isArray(body.slots)) { body.slots.forEach(slot => { - const bidRequest = find(request.bidRequests, b => b.adUnitCode === slot.impid && (!b.params.zoneId || parseInt(b.params.zoneId) === slot.zoneid)); - const bidId = bidRequest.bidId; - const bid = { - requestId: bidId, - cpm: slot.cpm, - currency: slot.currency, - netRevenue: true, - ttl: slot.ttl || 60, - creativeId: slot.creativecode, - width: slot.width, - height: slot.height, - dealId: slot.deal, - }; - if (body.ext?.paf?.transmission && slot.ext?.paf?.content_id) { - const pafResponseMeta = { - content_id: slot.ext.paf.content_id, - transmission: response.ext.paf.transmission + const bidRequest = getAssociatedBidRequest(request.bidRequests, slot); + if (bidRequest) { + const bidId = bidRequest.bidId; + const bid = { + requestId: bidId, + cpm: slot.cpm, + currency: slot.currency, + netRevenue: true, + ttl: slot.ttl || 60, + creativeId: slot.creativecode, + width: slot.width, + height: slot.height, + dealId: slot.deal, }; - bid.meta = Object.assign({}, bid.meta, { paf: pafResponseMeta }); - } - if (slot.adomain) { - bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [slot.adomain].flat() }); - } - if (slot.ext?.meta?.networkName) { - bid.meta = Object.assign({}, bid.meta, { networkName: slot.ext.meta.networkName }) - } - if (slot.native) { - if (bidRequest.params.nativeCallback) { - bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); - } else { - bid.native = createPrebidNativeAd(slot.native); - bid.mediaType = NATIVE; + if (body.ext?.paf?.transmission && slot.ext?.paf?.content_id) { + const pafResponseMeta = { + content_id: slot.ext.paf.content_id, + transmission: response.ext.paf.transmission + }; + bid.meta = Object.assign({}, bid.meta, { paf: pafResponseMeta }); } - } else if (slot.video) { - bid.vastUrl = slot.displayurl; - bid.mediaType = VIDEO; - const context = deepAccess(bidRequest, 'mediaTypes.video.context'); - // if outstream video, add a default render for it. - if (context === OUTSTREAM) { - bid.renderer = createOutstreamVideoRenderer(slot); + if (slot.adomain) { + bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [slot.adomain].flat() }); } - } else { - bid.ad = slot.creative; + if (slot.ext?.meta?.networkName) { + bid.meta = Object.assign({}, bid.meta, { networkName: slot.ext.meta.networkName }) + } + if (slot.native) { + if (bidRequest.params.nativeCallback) { + bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); + } else { + bid.native = createPrebidNativeAd(slot.native); + bid.mediaType = NATIVE; + } + } else if (slot.video) { + bid.vastUrl = slot.displayurl; + bid.mediaType = VIDEO; + const context = deepAccess(bidRequest, 'mediaTypes.video.context'); + // if outstream video, add a default render for it. + if (context === OUTSTREAM) { + bid.renderer = createOutstreamVideoRenderer(slot); + } + } else { + bid.ad = slot.creative; + } + bids.push(bid); } - bids.push(bid); }); } + if (isArray(body.ext?.igbid)) { + const seller = body.ext.seller || FLEDGE_SELLER_DOMAIN; + const sellerTimeout = body.ext.sellerTimeout || FLEDGE_SELLER_TIMEOUT; + const sellerSignals = body.ext.sellerSignals || {}; + body.ext.igbid.forEach((igbid) => { + const perBuyerSignals = {}; + igbid.igbuyer.forEach(buyerItem => { + perBuyerSignals[buyerItem.origin] = buyerItem.buyerdata; + }); + const bidRequest = request.bidRequests.find(b => b.bidId === igbid.impid); + if (!sellerSignals.floor && bidRequest.params.bidFloor) { + sellerSignals.floor = bidRequest.params.bidFloor; + } + if (!sellerSignals.sellerCurrency && bidRequest.params.bidFloorCur) { + sellerSignals.sellerCurrency = bidRequest.params.bidFloorCur; + } + const bidId = bidRequest.bidId; + fledgeAuctionConfigs.push({ + bidId, + config: { + seller, + sellerSignals, + sellerTimeout, + perBuyerSignals, + auctionSignals: {}, + decisionLogicUrl: FLEDGE_DECISION_LOGIC_URL, + interestGroupBuyers: Object.keys(perBuyerSignals), + }, + }); + }); + } + + if (fledgeAuctionConfigs.length) { + return { + bids, + fledgeAuctionConfigs, + }; + } + return bids; }, /** @@ -306,6 +351,23 @@ export const spec = { adapter.handleSetTargeting(bid); } }, + + /** + * @param {BidRequest[]} bidRequests + */ + onDataDeletionRequest: (bidRequests) => { + const id = readFromAllStorages(BUNDLE_COOKIE_NAME); + if (id) { + deleteFromAllStorages(BUNDLE_COOKIE_NAME); + ajax('https://privacy.criteo.com/api/privacy/datadeletionrequest', + null, + JSON.stringify({ publisherUserId: id }), + { + contentType: 'application/json', + method: 'POST' + }); + } + } }; function readFromAllStorages(name) { @@ -444,6 +506,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { networkId = bidRequest.params.networkId || networkId; schain = bidRequest.schain || schain; const slot = { + slotid: bidRequest.bidId, impid: bidRequest.adUnitCode, transactionid: bidRequest.ortb2Imp?.ext?.tid }; @@ -483,6 +546,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (hasVideoMediaType(bidRequest)) { const video = { + context: bidRequest.mediaTypes.video.context, playersizes: parseSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize'), parseSize), mimes: bidRequest.mediaTypes.video.mimes, protocols: bidRequest.mediaTypes.video.protocols, @@ -493,7 +557,19 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { minduration: bidRequest.mediaTypes.video.minduration, playbackmethod: bidRequest.mediaTypes.video.playbackmethod, startdelay: bidRequest.mediaTypes.video.startdelay, - plcmt: bidRequest.mediaTypes.video.plcmt + plcmt: bidRequest.mediaTypes.video.plcmt, + w: bidRequest.mediaTypes.video.w, + h: bidRequest.mediaTypes.video.h, + linearity: bidRequest.mediaTypes.video.linearity, + skipmin: bidRequest.mediaTypes.video.skipmin, + skipafter: bidRequest.mediaTypes.video.skipafter, + minbitrate: bidRequest.mediaTypes.video.minbitrate, + maxbitrate: bidRequest.mediaTypes.video.maxbitrate, + delivery: bidRequest.mediaTypes.video.delivery, + pos: bidRequest.mediaTypes.video.pos, + playbackend: bidRequest.mediaTypes.video.playbackend, + adPodDurationSec: bidRequest.mediaTypes.video.adPodDurationSec, + durationRangeSec: bidRequest.mediaTypes.video.durationRangeSec, }; const paramsVideo = bidRequest.params.video; if (paramsVideo !== undefined) { @@ -509,6 +585,10 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { enrichSlotWithFloors(slot, bidRequest); + if (!bidderRequest.fledgeEnabled && slot.ext?.ae) { + delete slot.ext.ae; + } + return slot; }), }; @@ -768,6 +848,27 @@ function createOutstreamVideoRenderer(slot) { return renderer; } +function getAssociatedBidRequest(bidRequests, slot) { + for (const request of bidRequests) { + if (request.adUnitCode === slot.impid) { + if (request.params.zoneId && parseInt(request.params.zoneId) === slot.zoneid) { + return request; + } else if (slot.native) { + if (request.mediaTypes?.native || request.nativeParams) { + return request; + } + } else if (slot.video) { + if (request.mediaTypes?.video) { + return request; + } + } else if (request.mediaTypes?.banner || request.sizes) { + return request; + } + } + } + return undefined; +} + export function tryGetCriteoFastBid() { // begin ref#1 try { diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index b48612203f8..6a09ce2c973 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -22,6 +22,9 @@ const bundleStorageKey = 'cto_bundle'; const dnaBundleStorageKey = 'cto_dna_bundle'; const cookiesMaxAge = 13 * 30 * 24 * 60 * 60 * 1000; +const STORAGE_TYPE_LOCALSTORAGE = 'html5'; +const STORAGE_TYPE_COOKIES = 'cookie'; + const pastDateString = new Date(0).toString(); const expirationString = new Date(timestamp() + cookiesMaxAge).toString(); @@ -32,14 +35,26 @@ function extractProtocolHost(url, returnOnlyHost = false) { : `${parsedUrl.protocol}://${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}/`; } -function getFromAllStorages(key) { +function getFromStorage(submoduleConfig, key) { + if (submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) { + return storage.getDataFromLocalStorage(key); + } else if (submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) { + return storage.getCookie(key); + } + return storage.getCookie(key) || storage.getDataFromLocalStorage(key); } -function saveOnAllStorages(key, value, hostname) { +function saveOnStorage(submoduleConfig, key, value, hostname) { if (key && value) { - storage.setDataInLocalStorage(key, value); - setCookieOnAllDomains(key, value, expirationString, hostname, true); + if (submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) { + storage.setDataInLocalStorage(key, value); + } else if (submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) { + setCookieOnAllDomains(key, value, expirationString, hostname, true); + } else { + storage.setDataInLocalStorage(key, value); + setCookieOnAllDomains(key, value, expirationString, hostname, true); + } } } @@ -70,11 +85,11 @@ function deleteFromAllStorages(key, hostname) { storage.removeDataFromLocalStorage(key); } -function getCriteoDataFromAllStorages() { +function getCriteoDataFromStorage(submoduleConfig) { return { - bundle: getFromAllStorages(bundleStorageKey), - dnaBundle: getFromAllStorages(dnaBundleStorageKey), - bidId: getFromAllStorages(bididStorageKey), + bundle: getFromStorage(submoduleConfig, bundleStorageKey), + dnaBundle: getFromStorage(submoduleConfig, dnaBundleStorageKey), + bidId: getFromStorage(submoduleConfig, bididStorageKey), } } @@ -108,7 +123,7 @@ function buildCriteoUsersyncUrl(topUrl, domain, bundle, dnaBundle, areCookiesWri return url; } -function callSyncPixel(domain, pixel) { +function callSyncPixel(submoduleConfig, domain, pixel) { if (pixel.writeBundleInStorage && pixel.bundlePropertyName && pixel.storageKeyName) { ajax( pixel.pixelUrl, @@ -117,7 +132,7 @@ function callSyncPixel(domain, pixel) { if (response) { const jsonResponse = JSON.parse(response); if (jsonResponse && jsonResponse[pixel.bundlePropertyName]) { - saveOnAllStorages(pixel.storageKeyName, jsonResponse[pixel.bundlePropertyName], domain); + saveOnStorage(submoduleConfig, pixel.storageKeyName, jsonResponse[pixel.bundlePropertyName], domain); } } }, @@ -133,9 +148,9 @@ function callSyncPixel(domain, pixel) { } } -function callCriteoUserSync(parsedCriteoData, callback) { - const cw = storage.cookiesAreEnabled(); - const lsw = storage.localStorageIsEnabled(); +function callCriteoUserSync(submoduleConfig, parsedCriteoData, callback) { + const cw = (submoduleConfig?.storage?.type === undefined || submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) && storage.cookiesAreEnabled(); + const lsw = (submoduleConfig?.storage?.type === undefined || submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) && storage.localStorageIsEnabled(); const topUrl = extractProtocolHost(getRefererInfo().page); // TODO: should domain really be extracted from the current frame? const domain = extractProtocolHost(document.location.href, true); @@ -156,18 +171,18 @@ function callCriteoUserSync(parsedCriteoData, callback) { const jsonResponse = JSON.parse(response); if (jsonResponse.pixels) { - jsonResponse.pixels.forEach(pixel => callSyncPixel(domain, pixel)); + jsonResponse.pixels.forEach(pixel => callSyncPixel(submoduleConfig, domain, pixel)); } if (jsonResponse.acwsUrl) { const urlsToCall = typeof jsonResponse.acwsUrl === 'string' ? [jsonResponse.acwsUrl] : jsonResponse.acwsUrl; urlsToCall.forEach(url => triggerPixel(url)); } else if (jsonResponse.bundle) { - saveOnAllStorages(bundleStorageKey, jsonResponse.bundle, domain); + saveOnStorage(submoduleConfig, bundleStorageKey, jsonResponse.bundle, domain); } if (jsonResponse.bidId) { - saveOnAllStorages(bididStorageKey, jsonResponse.bidId, domain); + saveOnStorage(submoduleConfig, bididStorageKey, jsonResponse.bidId, domain); const criteoId = { criteoId: jsonResponse.bidId }; callback(criteoId); } else { @@ -207,15 +222,21 @@ export const criteoIdSubmodule = { * @param {ConsentData} [consentData] * @returns {{id: {criteoId: string} | undefined}}} */ - getId() { - let localData = getCriteoDataFromAllStorages(); + getId(submoduleConfig) { + let localData = getCriteoDataFromStorage(submoduleConfig); - const result = (callback) => callCriteoUserSync(localData, callback); + const result = (callback) => callCriteoUserSync(submoduleConfig, localData, callback); return { id: localData.bidId ? { criteoId: localData.bidId } : undefined, callback: result } + }, + eids: { + 'criteoId': { + source: 'criteo.com', + atype: 1 + }, } }; diff --git a/modules/currency.js b/modules/currency.js index 3da0cfe73e8..e42799c0864 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -7,29 +7,24 @@ import {getHook} from '../src/hook.js'; import {defer} from '../src/utils/promise.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; +import {on as onEvent, off as offEvent} from '../src/events.js'; const DEFAULT_CURRENCY_RATE_URL = 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json?date=$$TODAY$$'; const CURRENCY_RATE_PRECISION = 4; -var bidResponseQueue = []; -var conversionCache = {}; -var currencyRatesLoaded = false; -var needToCallForCurrencyFile = true; -var adServerCurrency = 'USD'; +let ratesURL; +let bidResponseQueue = []; +let conversionCache = {}; +let currencyRatesLoaded = false; +let needToCallForCurrencyFile = true; +let adServerCurrency = 'USD'; export var currencySupportEnabled = false; export var currencyRates = {}; -var bidderCurrencyDefault = {}; -var defaultRates; +let bidderCurrencyDefault = {}; +let defaultRates; -export const ready = (() => { - let ctl; - function reset() { - ctl = defer(); - } - reset(); - return {done: () => ctl.resolve(), reset, promise: () => ctl.promise} -})(); +export let responseReady = defer(); /** * Configuration function for currency @@ -64,7 +59,7 @@ export const ready = (() => { * there is an error loading the config.conversionRateFile. */ export function setConfig(config) { - let url = DEFAULT_CURRENCY_RATE_URL; + ratesURL = DEFAULT_CURRENCY_RATE_URL; if (typeof config.rates === 'object') { currencyRates.conversions = config.rates; @@ -86,14 +81,14 @@ export function setConfig(config) { adServerCurrency = config.adServerCurrency; if (config.conversionRateFile) { logInfo('currency using override conversionRateFile:', config.conversionRateFile); - url = config.conversionRateFile; + ratesURL = config.conversionRateFile; } // see if the url contains a date macro // this is a workaround to the fact that jsdelivr doesn't currently support setting a 24-hour HTTP cache header // So this is an approach to let the browser cache a copy of the file each day // We should remove the macro once the CDN support a day-level HTTP cache setting - const macroLocation = url.indexOf('$$TODAY$$'); + const macroLocation = ratesURL.indexOf('$$TODAY$$'); if (macroLocation !== -1) { // get the date to resolve the macro const d = new Date(); @@ -104,10 +99,10 @@ export function setConfig(config) { const todaysDate = `${d.getFullYear()}${month}${day}`; // replace $$TODAY$$ with todaysDate - url = `${url.substring(0, macroLocation)}${todaysDate}${url.substring(macroLocation + 9, url.length)}`; + ratesURL = `${ratesURL.substring(0, macroLocation)}${todaysDate}${ratesURL.substring(macroLocation + 9, ratesURL.length)}`; } - initCurrency(url); + initCurrency(); } else { // currency support is disabled, setting defaults logInfo('disabling currency support'); @@ -128,20 +123,11 @@ function errorSettingsRates(msg) { } } -function initCurrency(url) { - conversionCache = {}; - currencySupportEnabled = true; - - logInfo('Installing addBidResponse decorator for currency module', arguments); - - // Adding conversion function to prebid global for external module and on page use - getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); - getHook('addBidResponse').before(addBidResponseHook, 100); - - // call for the file if we haven't already +function loadRates() { if (needToCallForCurrencyFile) { needToCallForCurrencyFile = false; - ajax(url, + currencyRatesLoaded = false; + ajax(ratesURL, { success: function (response) { try { @@ -150,26 +136,43 @@ function initCurrency(url) { conversionCache = {}; currencyRatesLoaded = true; processBidResponseQueue(); - ready.done(); } catch (e) { errorSettingsRates('Failed to parse currencyRates response: ' + response); } }, error: function (...args) { errorSettingsRates(...args); - ready.done(); + currencyRatesLoaded = true; + processBidResponseQueue(); + needToCallForCurrencyFile = true; } } ); - } else { - ready.done(); } } +function initCurrency() { + conversionCache = {}; + currencySupportEnabled = true; + + logInfo('Installing addBidResponse decorator for currency module', arguments); + + // Adding conversion function to prebid global for external module and on page use + getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); + getHook('addBidResponse').before(addBidResponseHook, 100); + getHook('responsesReady').before(responsesReadyHook); + onEvent(CONSTANTS.EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + onEvent(CONSTANTS.EVENTS.AUCTION_INIT, loadRates); + loadRates(); +} + function resetCurrency() { logInfo('Uninstalling addBidResponse decorator for currency module', arguments); getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove(); + offEvent(CONSTANTS.EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + offEvent(CONSTANTS.EVENTS.AUCTION_INIT, loadRates); delete getGlobal().convertCurrency; adServerCurrency = 'USD'; @@ -179,6 +182,11 @@ function resetCurrency() { needToCallForCurrencyFile = true; currencyRates = {}; bidderCurrencyDefault = {}; + responseReady = defer(); +} + +function responsesReadyHook(next, ready) { + next(ready.then(() => responseReady.promise)); } export const addBidResponseHook = timedBidResponseHook('currency', function addBidResponseHook(fn, adUnitCode, bid, reject) { @@ -211,24 +219,25 @@ export const addBidResponseHook = timedBidResponseHook('currency', function addB if (bid.currency === adServerCurrency) { return fn.call(this, adUnitCode, bid, reject); } - - bidResponseQueue.push(wrapFunction(fn, this, [adUnitCode, bid, reject])); + bidResponseQueue.push([fn, this, adUnitCode, bid, reject]); if (!currencySupportEnabled || currencyRatesLoaded) { processBidResponseQueue(); - } else { - fn.untimed.bail(ready.promise()); } }); -function processBidResponseQueue() { - while (bidResponseQueue.length > 0) { - (bidResponseQueue.shift())(); - } +function rejectOnAuctionTimeout({auctionId}) { + bidResponseQueue = bidResponseQueue.filter(([fn, ctx, adUnitCode, bid, reject]) => { + if (bid.auctionId === auctionId) { + reject(CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY) + } else { + return true; + } + }); } -function wrapFunction(fn, context, params) { - return function() { - let bid = params[1]; +function processBidResponseQueue() { + while (bidResponseQueue.length > 0) { + const [fn, ctx, adUnitCode, bid, reject] = bidResponseQueue.shift(); if (bid !== undefined && 'currency' in bid && 'cpm' in bid) { let fromCurrency = bid.currency; try { @@ -239,12 +248,13 @@ function wrapFunction(fn, context, params) { } } catch (e) { logWarn('getCurrencyConversion threw error: ', e); - params[2](CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); - return; + reject(CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + continue; } } - return fn.apply(context, params); - }; + fn.call(ctx, adUnitCode, bid, reject); + } + responseReady.resolve(); } function getCurrencyConversion(fromCurrency, toCurrency = adServerCurrency) { diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index 604d7235d0f..f158e16a64e 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -2,6 +2,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {BANNER} from '../src/mediaTypes.js'; import {generateUUID, getParameterByName, isNumber, logError, logInfo} from '../src/utils.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; // ------------------------------------ const BIDDER_CODE = 'cwire'; @@ -230,5 +231,23 @@ export const spec = { navigator.sendBeacon(EVENT_ENDPOINT, JSON.stringify(event)) }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + logInfo('Collecting user-syncs: ', JSON.stringify({syncOptions, gdprConsent, uspConsent, serverResponses})); + + const syncs = [] + if (hasPurpose1Consent(gdprConsent)) { + logInfo('GDPR purpose 1 consent was given, adding user-syncs') + let type = (syncOptions.pixelEnabled) ? 'image' : null ?? (syncOptions.iframeEnabled) ? 'iframe' : null + if (type) { + syncs.push({ + type: type, + url: 'https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID' + }) + } + } + logInfo('Collected user-syncs: ', JSON.stringify({syncs})) + return syncs + } + }; registerBidder(spec); diff --git a/modules/czechAdIdSystem.js b/modules/czechAdIdSystem.js index 957b3ed30bd..ae958aae198 100644 --- a/modules/czechAdIdSystem.js +++ b/modules/czechAdIdSystem.js @@ -42,6 +42,12 @@ export const czechAdIdSubmodule = { getId () { const id = readId() return id ? { id: id } : undefined + }, + eids: { + 'czechAdId': { + source: 'czechadid.cz', + atype: 1 + }, } } diff --git a/modules/dacIdSystem.js b/modules/dacIdSystem.js index 5adca074c87..ffdadef18e8 100644 --- a/modules/dacIdSystem.js +++ b/modules/dacIdSystem.js @@ -170,6 +170,12 @@ export const dacIdSystemSubmodule = { } return fetchAoneId(configParams.oid, cookie.fuuid); + }, + eids: { + 'dacId': { + source: 'impact-ad.jp', + atype: 1 + }, } }; diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 11d3ebb1589..395706994fe 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -1,10 +1,11 @@ -import {deepAccess, getAdUnitSizes, getWindowTop, isEmpty, isGptPubadsDefined} from '../src/utils.js'; +import {deepAccess, getWindowTop, isEmpty, isGptPubadsDefined} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; export const storage = getStorageManager({bidderCode: 'datablocks'}); diff --git a/modules/datawrkzBidAdapter.js b/modules/datawrkzBidAdapter.js index 2cf28c36330..127e7893ec5 100644 --- a/modules/datawrkzBidAdapter.js +++ b/modules/datawrkzBidAdapter.js @@ -1,4 +1,12 @@ -import { deepAccess, getBidIdParameter, isArray, getUniqueIdentifierStr, contains, isFn, isPlainObject } from '../src/utils.js'; +import { + deepAccess, + isArray, + getUniqueIdentifierStr, + contains, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; diff --git a/modules/dchain.js b/modules/dchain.js index daf97a7551f..7f84282b81e 100644 --- a/modules/dchain.js +++ b/modules/dchain.js @@ -1,7 +1,7 @@ import {includes} from '../src/polyfill.js'; import {config} from '../src/config.js'; import {getHook} from '../src/hook.js'; -import {_each, deepAccess, deepClone, hasOwn, isArray, isPlainObject, isStr, logError, logWarn} from '../src/utils.js'; +import {_each, deepAccess, deepClone, isArray, isPlainObject, isStr, logError, logWarn} from '../src/utils.js'; import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; const shouldBeAString = ' should be a string'; @@ -49,7 +49,7 @@ export function checkDchainSyntax(bid, mode) { appendFailMsg(`dchain.ver` + shouldBeAString); } - if (hasOwn(dchainObj, 'ext')) { + if (dchainObj.hasOwnProperty('ext')) { if (!isPlainObject(dchainObj.ext)) { appendFailMsg(`dchain.ext` + shouldBeAnObject); } diff --git a/modules/deepintentDpesIdSystem.js b/modules/deepintentDpesIdSystem.js index f2b50d535eb..4d685592c04 100644 --- a/modules/deepintentDpesIdSystem.js +++ b/modules/deepintentDpesIdSystem.js @@ -39,8 +39,13 @@ export const deepintentDpesSubmodule = { */ getId(config, consentData, cacheIdObj) { return cacheIdObj; - } - + }, + eids: { + 'deepintentId': { + source: 'deepintent.com', + atype: 3 + }, + }, }; submodule('userId', deepintentDpesSubmodule); diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 3394fd8b3f4..a3e26dc7202 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -249,7 +249,9 @@ export function buildAdpodVideoUrl({code, params, callback} = {}) { */ function buildUrlFromAdserverUrlComponents(components, bid, options) { const descriptionUrl = getDescriptionUrl(bid, components, 'search'); - if (descriptionUrl) { components.search.description_url = descriptionUrl; } + if (descriptionUrl) { + components.search.description_url = descriptionUrl; + } components.search.cust_params = getCustParams(bid, options, components.search.cust_params); return buildUrl(components); @@ -264,7 +266,7 @@ function buildUrlFromAdserverUrlComponents(components, bid, options) { * @return {string | undefined} The encoded vast url if it exists, or undefined */ function getDescriptionUrl(bid, components, prop) { - return deepAccess(components, `${prop}.description_url`) || dep.ri().page; + return deepAccess(components, `${prop}.description_url`) || encodeURIComponent(dep.ri().page); } /** diff --git a/modules/dgkeywordRtdProvider.js b/modules/dgkeywordRtdProvider.js index 99df3b18a39..76f5f04ac03 100644 --- a/modules/dgkeywordRtdProvider.js +++ b/modules/dgkeywordRtdProvider.js @@ -6,8 +6,7 @@ * @module modules/dgkeywordProvider * @requires module:modules/realTimeData */ - -import {logMessage, deepSetValue, logError, logInfo, mergeDeep} from '../src/utils.js'; +import { logMessage, deepSetValue, logError, logInfo, isStr, isArray } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { getGlobal } from '../src/prebidGlobal.js'; @@ -57,20 +56,12 @@ export function getDgKeywordsAndSet(reqBidsConfigObj, callback, moduleConfig, us if (Object.keys(keywords).length > 0) { const targetBidKeys = {}; for (let bid of setKeywordTargetBidders) { - // set keywords to params - bid.params.keywords = keywords; + // set keywords to ortb2Imp + deepSetValue(bid, 'ortb2Imp.ext.data.keywords', convertKeywordsToString(keywords)); if (!targetBidKeys[bid.bidder]) { targetBidKeys[bid.bidder] = true; } } - - if (!reqBidsConfigObj._ignoreSetOrtb2) { - // set keywrods to ortb2 - let addOrtb2 = {}; - deepSetValue(addOrtb2, 'site.keywords', keywords); - deepSetValue(addOrtb2, 'user.keywords', keywords); - mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, Object.fromEntries(Object.keys(targetBidKeys).map(bidder => [bidder, addOrtb2]))); - } } } isFinish = true; @@ -156,4 +147,37 @@ function init(moduleConfig) { function registerSubModule() { submodule('realTimeData', dgkeywordSubmodule); } + +// keywords: { 'genre': ['rock', 'pop'], 'pets': ['dog'] } goes to 'genre=rock,genre=pop,pets=dog' +export function convertKeywordsToString(keywords) { + let result = ''; + Object.keys(keywords).forEach(key => { + // if 'text' or '' + if (isStr(keywords[key])) { + if (keywords[key] !== '') { + result += `${key}=${keywords[key]},` + } else { + result += `${key},`; + } + } else if (isArray(keywords[key])) { + let isValSet = false + keywords[key].forEach(val => { + if (isStr(val) && val) { + result += `${key}=${val},` + isValSet = true + } + }); + if (!isValSet) { + result += `${key},` + } + } else { + result += `${key},` + } + }); + + // remove last trailing comma + result = result.substring(0, result.length - 1); + return result; +} + registerSubModule(); diff --git a/modules/discoveryBidAdapter.js b/modules/discoveryBidAdapter.js index 675ede273a6..816ec3cbe1f 100644 --- a/modules/discoveryBidAdapter.js +++ b/modules/discoveryBidAdapter.js @@ -211,6 +211,64 @@ const popInAdSize = [ { w: 336, h: 280 }, ]; +/** + * get screen size + * + * @returns {Array} eg: "['widthxheight']" + */ +function getScreenSize() { + return utils.parseSizesInput([window.screen.width, window.screen.height]); +} + +/** + * @param {BidRequest} bidRequest + * @param bidderRequest + * @returns {string} + */ +function getReferrer(bidRequest = {}, bidderRequest = {}) { + let pageUrl; + if (bidRequest.params && bidRequest.params.referrer) { + pageUrl = bidRequest.params.referrer; + } else { + pageUrl = utils.deepAccess(bidderRequest, 'refererInfo.page'); + } + return pageUrl; +} + +/** + * format imp ad test ext params + * + * @param validBidRequest sigleBidRequest + * @param bidderRequest + */ +function addImpExtParams(bidRequest = {}, bidderRequest = {}) { + const { deepAccess } = utils; + const { params = {}, adUnitCode, bidId } = bidRequest; + const ext = { + bidId: bidId || '', + adUnitCode: adUnitCode || '', + token: params.token || '', + siteId: params.siteId || '', + zoneId: params.zoneId || '', + publisher: params.publisher || '', + p_pos: params.position || '', + screenSize: getScreenSize(), + referrer: getReferrer(bidRequest, bidderRequest), + stack: deepAccess(bidRequest, 'refererInfo.stack', []), + b_pos: deepAccess(bidRequest, 'mediaTypes.banner.pos', '', ''), + ortbUser: deepAccess(bidRequest, 'ortb2.user', {}, {}), + ortbSite: deepAccess(bidRequest, 'ortb2.site', {}, {}), + tid: deepAccess(bidRequest, 'ortb2Imp.ext.tid', '', ''), + browsiViewability: deepAccess(bidRequest, 'ortb2Imp.ext.data.browsi.browsiViewability', '', ''), + adserverName: deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.name', '', ''), + adslot: deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.adslot', '', ''), + keywords: deepAccess(bidRequest, 'ortb2Imp.ext.data.keywords', '', ''), + gpid: deepAccess(bidRequest, 'ortb2Imp.ext.gpid', '', ''), + pbadslot: deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot', '', ''), + }; + return ext; +} + /** * get aditem setting * @param {Array} validBidRequests an an array of bids @@ -258,9 +316,14 @@ function getItems(validBidRequests, bidderRequest) { format: sizes, }, ext: {}, - tagid: globals['tagid'], + tagid: req.params && req.params.tagid }; } + + try { + ret.ext = addImpExtParams(req, bidderRequest); + } catch (e) {} + itemMaps[id] = { req, ret, @@ -323,7 +386,6 @@ function getParam(validBidRequests, bidderRequest) { buyeruid: getUserID(), id: sharedid || pubcid, }, - eids, tmax: timeout, site: { name: domain, diff --git a/modules/displayioBidAdapter.js b/modules/displayioBidAdapter.js index 3d34f2c8b26..3cdfd3a77cd 100644 --- a/modules/displayioBidAdapter.js +++ b/modules/displayioBidAdapter.js @@ -1,7 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; -import {getWindowFromDocument, logWarn} from '../src/utils.js'; +import {logWarn} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; @@ -156,7 +156,7 @@ function newRenderer(bid) { function webisRender(bid, doc) { bid.renderer.push(() => { - const win = getWindowFromDocument(doc) || window; + const win = doc?.defaultView || window; win.webis.init(bid.adData, bid.adUnitCode, bid.params); }) } diff --git a/modules/dmdIdSystem.js b/modules/dmdIdSystem.js index b42315d66ee..2f910a8bd92 100644 --- a/modules/dmdIdSystem.js +++ b/modules/dmdIdSystem.js @@ -85,6 +85,12 @@ export const dmdIdSubmodule = { }; return { callback: resp }; } + }, + eids: { + 'dmdId': { + source: 'hcn.health', + atype: 3 + }, } }; diff --git a/modules/docereeAdManagerBidAdapter.js b/modules/docereeAdManagerBidAdapter.js new file mode 100644 index 00000000000..d3765f5a130 --- /dev/null +++ b/modules/docereeAdManagerBidAdapter.js @@ -0,0 +1,128 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; +const BIDDER_CODE = 'docereeadmanager'; +const END_POINT = 'https://dai.doceree.com/drs/quest'; + +export const spec = { + code: BIDDER_CODE, + url: '', + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bid) => { + const { placementId } = bid.params; + return !!placementId; + }, + isGdprConsentPresent: (bid) => { + const { gdpr, gdprconsent } = bid.params; + if (gdpr == '1') { + return !!gdprconsent; + } + return true; + }, + buildRequests: (validBidRequests) => { + const serverRequests = []; + const { data } = config.getConfig('docereeadmanager.user') || {}; + + validBidRequests.forEach(function (validBidRequest) { + const payload = getPayload(validBidRequest, data); + + if (!payload) { + return; + } + + serverRequests.push({ + method: 'POST', + url: END_POINT, + data: JSON.stringify(payload.data), + options: { + contentType: 'application/json', + withCredentials: true, + }, + }); + }); + + return serverRequests; + }, + interpretResponse: (serverResponse) => { + const responseJson = serverResponse ? serverResponse.body : {}; + const bidResponse = { + ad: responseJson.ad, + width: Number(responseJson.width), + height: Number(responseJson.height), + requestId: responseJson.requestId, + netRevenue: true, + ttl: 30, + cpm: responseJson.cpm, + currency: responseJson.currency, + mediaType: BANNER, + creativeId: responseJson.creativeId, + meta: { + advertiserDomains: + Array.isArray(responseJson.meta.advertiserDomains) && + responseJson.meta.advertiserDomains.length > 0 + ? responseJson.meta.advertiserDomains + : [], + }, + }; + + return [bidResponse]; + }, +}; + +function getPayload(bid, userData) { + if (!userData || !bid) { + return false; + } + + const { bidId, params } = bid; + const { placementId } = params; + const { + userid, + email, + firstname, + lastname, + specialization, + hcpid, + gender, + city, + state, + zipcode, + hashedNPI, + hashedhcpid, + hashedemail, + hashedmobile, + country, + organization, + dob, + } = userData; + + const data = { + userid: userid || '', + email: email || '', + firstname: firstname || '', + lastname: lastname || '', + specialization: specialization || '', + hcpid: hcpid || '', + gender: gender || '', + city: city || '', + state: state || '', + zipcode: zipcode || '', + hashedNPI: hashedNPI || '', + pb: 1, + adunit: placementId || '', + requestId: bidId || '', + hashedhcpid: hashedhcpid || '', + hashedemail: hashedemail || '', + hashedmobile: hashedmobile || '', + country: country || '', + organization: organization || '', + dob: dob || '', + userconsent: 1, + }; + return { + data, + }; +} + +registerBidder(spec); diff --git a/modules/docereeAdManagerBidAdapter.md b/modules/docereeAdManagerBidAdapter.md new file mode 100644 index 00000000000..bedbf57b179 --- /dev/null +++ b/modules/docereeAdManagerBidAdapter.md @@ -0,0 +1,68 @@ +# Overview + +``` +Module Name: Doceree AdManager Bidder Adapter +Module Type: Bidder Adapter +Maintainer: tech.stack@doceree.com +``` + + + +Connects to Doceree demand source to fetch bids. +Please use `docereeadmanager` as the bidder code. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'DOC-397-1', + sizes: [ + [300, 250] + ], + bids: [ + { + bidder: 'docereeadmanager', + params: { + placementId: 'DOC-19-1', //required + publisherUrl: document.URL || window.location.href, //optional + gdpr: '1', //optional + gdprconsent:'CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g', //optional + } + } + ] + } +]; +``` + +```javascript +pbjs.setBidderConfig({ + bidders: ['docereeadmanager'], + config: { + docereeadmanager: { + user: { + data: { + email: 'XXX.XXX@GMAIL.COM', + firstname: 'DR. XXX', + lastname: 'XXX', + mobile: '981234XXXX', + specialization: 'Internal Medicine', + organization: 'Max Lifecare', + hcpid: '199291XXXX', + dob: '1987-08-27', + gender: 'Female', + city: 'Oildale', + state: 'California', + country: 'California', + hashedhcpid: '', + hashedemail: '', + hashedmobile: '', + userid: '7d26d8ca-233a-46c2-9d36-7c5d261e151d', + zipcode: '', + userconsent: '1', + }, + }, + }, + }, +}); +``` diff --git a/modules/docereeBidAdapter.js b/modules/docereeBidAdapter.js index 524f464cee3..2731e1ff397 100644 --- a/modules/docereeBidAdapter.js +++ b/modules/docereeBidAdapter.js @@ -1,9 +1,11 @@ -import { tryAppendQueryString } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { triggerPixel } from '../src/utils.js'; import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'doceree'; const END_POINT = 'https://bidder.doceree.com' +const TRACKING_END_POINT = 'https://tracking.doceree.com' export const spec = { code: BIDDER_CODE, @@ -69,6 +71,33 @@ export const spec = { } }; return [bidResponse]; + }, + onTimeout: function(timeoutData) { + if (timeoutData == null || !timeoutData.length) { + return; + } + timeoutData.forEach(td => { + const encodedBuf = window.btoa(encodeURIComponent(JSON.stringify({ + bidId: td.bidId, + timeout: td.timeout, + }))); + triggerPixel(TRACKING_END_POINT + '/v1/hbTimeout?adp=prebidjs&data=' + encodedBuf); + }) + }, + onBidWon: function (bidWon) { + if (bidWon == null) { + return; + } + const encodedBuf = window.btoa(encodeURIComponent(JSON.stringify({ + requestId: bidWon.requestId, + cpm: bidWon.cpm, + adId: bidWon.adId, + currency: bidWon.currency, + netRevenue: bidWon.netRevenue, + status: bidWon.status, + hb_pb: bidWon.adserverTargeting && bidWon.adserverTargeting.hb_pb, + }))); + triggerPixel(TRACKING_END_POINT + '/v1/hbBidWon?adp=prebidjs&data=' + encodedBuf); } }; diff --git a/modules/dxkultureBidAdapter.js b/modules/dxkultureBidAdapter.js new file mode 100644 index 00000000000..9e4768d12bb --- /dev/null +++ b/modules/dxkultureBidAdapter.js @@ -0,0 +1,278 @@ +import { + logInfo, + logError, + logMessage, + deepAccess, + deepSetValue, + mergeDeep +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +const BIDDER_CODE = 'dxkulture'; +const DEFAULT_BID_TTL = 300; +const DEFAULT_NET_REVENUE = true; +const DEFAULT_CURRENCY = 'USD'; +const SYNC_URL = 'https://ads.kulture.media/usync'; + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + if (!imp.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.bidfloorcur = bidRequest.params.currency || DEFAULT_CURRENCY; + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + mergeDeep(req, { + ext: { + hb: 1, + prebidver: '$prebid.version$', + adapterver: '1.0.0', + } + }) + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + deepSetValue(req, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(req, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + deepSetValue(req, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + return req; + }, + bidResponse(buildBidResponse, bid, context) { + let resMediaType; + if (bid.adm?.trim().startsWith(' { + let div = document.getElementById(divId); + if (div) { + observer.observe(div); + } + }); + + let dabDateNow = new Date(); + let dabTimeNow = dabDateNow.getTime(); + let dabElapsedSeconds = Math.floor((dabTimeNow - dabStartTime) / 1000); + let elapsedThreshold = 30; + if (config.params.threshold) { + elapsedThreshold = config.params.threshold; + } + if (dabElapsedSeconds >= elapsedThreshold) { + clearInterval(dabInterval); // Stop + loadLmScript(keyId); + } + }, 1000); + + return true; + } + } + return false; +} + +function loadLmScript(keyId) { + let viewableAdUnits = Object.keys(dynamicAdBoostAdUnits); + let viewableAdUnitsCSV = viewableAdUnits.join(','); + const scriptUrl = `${SCRIPT_URL}/${keyId}.js?viewableAdUnits=${viewableAdUnitsCSV}`; + loadExternalScript(scriptUrl, MODULE_NAME); + observer.disconnect(); +} + +function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { + const reqAdUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + + if (Array.isArray(reqAdUnits)) { + reqAdUnits.forEach(adunit => { + let gptCode = deepAccess(adunit, 'code'); + if (dynamicAdBoostAdUnits.hasOwnProperty(gptCode)) { + // AdUnits has reached target viewablity at some point + deepSetValue(adunit, `ortb2Imp.ext.data.${MODULE_NAME}.${gptCode}`, dynamicAdBoostAdUnits[gptCode]); + } + }); + } + callback(); +} + +let markViewed = (entry, observer) => { + return () => { + observer.unobserve(entry.target); + } +} + +// Callback function when an observed element becomes visible +function dabHandleIntersection(entries) { + entries.forEach(entry => { + if (entry.isIntersecting && entry.intersectionRatio > 0.5) { + dynamicAdBoostAdUnits[entry.target.id] = entry.intersectionRatio; + markViewed(entry, observer) + } + }); +} + +/** @type {RtdSubmodule} */ +export const subModuleObj = { + name: MODULE_NAME, + init, + getBidRequestData, + markViewed +}; + +submodule('realTimeData', subModuleObj); diff --git a/modules/dynamicAdBoostRtdProvider.md b/modules/dynamicAdBoostRtdProvider.md new file mode 100644 index 00000000000..93efe3b3f97 --- /dev/null +++ b/modules/dynamicAdBoostRtdProvider.md @@ -0,0 +1,40 @@ +# Overview + +Module Name: Dynamic Ad Boost +Module Type: Track when a adunit is viewable +Maintainer: info@luponmedia.com + +# Description + +Enhance your revenue with the cutting-edge DynamicAdBoost module! By seamlessly integrating the powerful LuponMedia technology, our module retrieves adunits viewability data, providing publishers with valuable insights to optimize their revenue streams. To unlock the full potential of this technology, we provide a customized LuponMedia module tailored to your specific site requirements. Boost your ad revenue and gain unprecedented visibility into your performance with our advanced solution. + +In order to utilize this module, it is essential to collaborate with [LuponMedia](https://www.luponmedia.com/) to create an account and obtain detailed guidelines on configuring your sites. Working hand in hand with LuponMedia will ensure a smooth integration process, enabling you to fully leverage the capabilities of this module on your website. Take the first step towards optimizing your ad revenue and enhancing your site's performance by partnering with LuponMedia for a seamless experience. +Contact info@luponmedia.com for information. + +## Building Prebid with Real-time Data Support + +First, make sure to add the Dynamic AdBoost submodule to your Prebid.js package with: + +`gulp build --modules=rtdModule,dynamicAdBoostRtdProvider` + +The following configuration parameters are available: + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: 2000, + dataProviders: [ + { + name: "dynamicAdBoost", + params: { + keyId: "[PROVIDED_KEY]", // Your provided Dynamic AdBoost keyId + adUnits: ["allowedAdUnit1", "allowedAdUnit2"], + threshold: 35 // optional + } + } + ] + } + ... +} +``` diff --git a/modules/ebdrBidAdapter.js b/modules/ebdrBidAdapter.js index a03a1ec12ca..e830f8a94f7 100644 --- a/modules/ebdrBidAdapter.js +++ b/modules/ebdrBidAdapter.js @@ -1,4 +1,4 @@ -import { logInfo, getBidIdParameter } from '../src/utils.js'; +import {getBidIdParameter, logInfo} from '../src/utils.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'ebdr'; diff --git a/modules/edge226BidAdapter.js b/modules/edge226BidAdapter.js new file mode 100644 index 00000000000..6d1e2466abe --- /dev/null +++ b/modules/edge226BidAdapter.js @@ -0,0 +1,188 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'edge226'; +const AD_URL = 'https://ssp.dauup.com/pbjs'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + } +}; + +registerBidder(spec); diff --git a/modules/edge226BidAdapter.md b/modules/edge226BidAdapter.md new file mode 100644 index 00000000000..b38ff67065f --- /dev/null +++ b/modules/edge226BidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Edge226 Bidder Adapter +Module Type: Edge226 Bidder Adapter +Maintainer: audit@edge226.com +``` + +# Description + +Connects to Edge226 exchange for bids. +Edge226 bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index 2216ab329b0..d57804c04e6 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -1,8 +1,9 @@ -import {getWindowSelf, isEmpty, parseSizesInput, isGptPubadsDefined, isSlotMatchingAdUnitCode} from '../src/utils.js'; +import {getWindowSelf, isEmpty, parseSizesInput, isGptPubadsDefined} from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'eplanning'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); @@ -267,6 +268,21 @@ function cleanName(name) { return name.replace(/_|\.|-|\//g, '').replace(/\)\(|\(|\)|:/g, '_').replace(/^_+|_+$/g, ''); } +function getFloorStr(bid) { + if (typeof bid.getFloor === 'function') { + let bidFloor = bid.getFloor({ + currency: DOLLAR_CODE, + mediaType: '*', + size: '*' + }); + + if (bidFloor.floor) { + return '|' + encodeURIComponent(bidFloor.floor); + } + } + return ''; +} + function getSpaces(bidRequests, ml) { let impType = bidRequests.reduce((previousBits, bid) => (bid.mediaTypes && bid.mediaTypes[VIDEO]) ? (bid.mediaTypes[VIDEO].context == 'outstream' ? (previousBits | 2) : (previousBits | 1)) : previousBits, 0); // Only one type of auction is supported at a time @@ -286,7 +302,7 @@ function getSpaces(bidRequests, ml) { let sizeVast = firstSize ? firstSize.join('x') : DEFAULT_SIZE_VAST; name = 'video_' + sizeVast + '_' + i; es.map[name] = bid.bidId; - return name + ':' + sizeVast + ';1'; + return name + ':' + sizeVast + ';1' + getFloorStr(bid); } if (ml) { @@ -296,7 +312,7 @@ function getSpaces(bidRequests, ml) { } es.map[name] = bid.bidId; - return name + ':' + getSize(bid); + return name + ':' + getSize(bid) + getFloorStr(bid); }).join('+')).join('+'); return es; } diff --git a/modules/eskimiBidAdapter.js b/modules/eskimiBidAdapter.js index 88d8f95b859..81b8c5d8058 100644 --- a/modules/eskimiBidAdapter.js +++ b/modules/eskimiBidAdapter.js @@ -1,7 +1,8 @@ -import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import * as utils from '../src/utils.js'; +import {getBidIdParameter} from '../src/utils.js'; const BIDDER_CODE = 'eskimi'; // const ENDPOINT = 'https://hb.eskimi.com/bids' @@ -65,7 +66,7 @@ const CONVERTER = ortbConverter({ imp.secure = Number(window.location.protocol === 'https:'); if (!imp.bidfloor && bidRequest.params.bidFloor) { imp.bidfloor = bidRequest.params.bidFloor; - imp.bidfloorcur = utils.getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' + imp.bidfloorcur = getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' } if (bidRequest.mediaTypes[VIDEO]) { diff --git a/modules/euidIdSystem.js b/modules/euidIdSystem.js index c4e15b11d15..6a3a0869c0e 100644 --- a/modules/euidIdSystem.js +++ b/modules/euidIdSystem.js @@ -103,6 +103,15 @@ export const euidIdSubmodule = { _logInfo(`EUID getId returned`, result); return result; }, + eids: { + 'euid': { + source: 'euid.eu', + atype: 3, + getValue: function(data) { + return data.id; + } + }, + }, }; function decodeImpl(value) { diff --git a/modules/experianRtdProvider.js b/modules/experianRtdProvider.js new file mode 100644 index 00000000000..e18296342de --- /dev/null +++ b/modules/experianRtdProvider.js @@ -0,0 +1,135 @@ +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { + deepAccess, + isArray, + isPlainObject, + isStr, + mergeDeep, + safeJSONParse, + timestamp +} from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; + +export const SUBMODULE_NAME = 'experian_rtid'; +export const EXPERIAN_RTID_DATA_KEY = 'experian_rtid_data'; +export const EXPERIAN_RTID_EXPIRATION_KEY = 'experian_rtid_expiration'; +export const EXPERIAN_RTID_STALE_KEY = 'experian_rtid_stale'; +export const EXPERIAN_RTID_NO_TRACK_KEY = 'experian_rtid_no_track'; +const EXPERIAN_RTID_URL = 'https://rtid.tapad.com' +const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); + +export const experianRtdObj = { + /** + * @summary modify bid request data + * @param {Object} reqBidsConfigObj + * @param {function} done + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + getBidRequestData(reqBidsConfigObj, done, config, userConsent) { + const dataEnvelope = storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null); + const stale = storage.getDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY, null); + const expired = storage.getDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, null); + const noTrack = storage.getDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null); + const now = timestamp() + if (now > new Date(expired).getTime() || (noTrack == null && dataEnvelope == null)) { + // request data envelope and don't manipulate bids + experianRtdObj.requestDataEnvelope(config, userConsent) + done(); + return false; + } + if (now > new Date(stale).getTime()) { + // request data envelope and manipulate bids + experianRtdObj.requestDataEnvelope(config, userConsent); + } + if (noTrack != null) { + done(); + return false; + } + experianRtdObj.alterBids(reqBidsConfigObj, config); + done() + return true; + }, + + alterBids(reqBidsConfigObj, config) { + const dataEnvelope = safeJSONParse(storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null)); + if (dataEnvelope == null) { + return; + } + deepAccess(config, 'params.bidders').forEach((bidderCode) => { + const bidderData = dataEnvelope.find(({ bidder }) => bidder === bidderCode) + if (bidderData != null) { + mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, { [bidderCode]: { experianRtidKey: bidderData.data.key, experianRtidData: bidderData.data.data } }) + } + }) + }, + requestDataEnvelope(config, userConsent) { + function storeDataEnvelopeResponse(response) { + const responseJson = safeJSONParse(response); + if (responseJson != null) { + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, responseJson.staleAt, null); + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, responseJson.expiresAt, null); + if (responseJson.status === 'no_track') { + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null); + storage.removeDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null); + } else { + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify(responseJson.data), null); + storage.removeDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null); + } + } + } + const queryString = experianRtdObj.extractConsentQueryString(config, userConsent) + const fullUrl = queryString == null ? `${EXPERIAN_RTID_URL}/acc/${deepAccess(config, 'params.accountId')}/ids` : `${EXPERIAN_RTID_URL}/acc/${deepAccess(config, 'params.accountId')}/ids${queryString}` + ajax(fullUrl, storeDataEnvelopeResponse, null, { withCredentials: true, contentType: 'application/json' }) + }, + extractConsentQueryString(config, userConsent) { + const queryObj = {}; + + if (userConsent != null) { + if (userConsent.gdpr != null) { + const { gdprApplies, consentString } = userConsent.gdpr; + mergeDeep(queryObj, {gdpr: gdprApplies, gdpr_consent: consentString}) + } + if (userConsent.uspConsent != null) { + mergeDeep(queryObj, {us_privacy: userConsent.uspConsent}) + } + } + const consentQueryString = Object.entries(queryObj).map(([key, val]) => `${key}=${val}`).join('&'); + + let idsString = ''; + if (deepAccess(config, 'params.ids') != null && isPlainObject(deepAccess(config, 'params.ids'))) { + idsString = Object.entries(deepAccess(config, 'params.ids')).map(([idType, val]) => { + if (isArray(val)) { + return val.map((singleVal) => `id.${idType}=${singleVal}`).join('&') + } else { + return `id.${idType}=${val}` + } + }).join('&') + } + + const combinedString = [consentQueryString, idsString].filter((string) => string !== '').join('&'); + return combinedString !== '' ? `?${combinedString}` : undefined; + }, + /** + * @function + * @summary init sub module + * @name RtdSubmodule#init + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + * @return {boolean} false to remove sub module + */ + init(config, userConsent) { + return isStr(deepAccess(config, 'params.accountId')); + } +} + +/** @type {RtdSubmodule} */ +export const experianRtdSubmodule = { + name: SUBMODULE_NAME, + getBidRequestData: experianRtdObj.getBidRequestData, + init: experianRtdObj.init +} + +submodule('realTimeData', experianRtdSubmodule); diff --git a/modules/experianRtdProvider.md b/modules/experianRtdProvider.md new file mode 100644 index 00000000000..ad46e0c3d55 --- /dev/null +++ b/modules/experianRtdProvider.md @@ -0,0 +1,52 @@ +# Experian Real-time Data Submodule + +## Overview + + Module Name: Experian Rtd Provider + Module Type: Rtd Provider + Maintainer: team-ui@tapad.com + +## Description + +The Experian RTD module adds encrypted identifier envelope to the bidding object. + +## Usage + +### Build +``` +gulp build --modules="rtdModule,experianRtdProvider,appnexusBidAdapter,..." +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Experian RTD module. + +### Configuration + +Use `setConfig` to instruct Prebid.js to initialize the Experian RTD module, as specified below. + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [{ + name: 'experian_rtid', + waitForIt: true, + params: { + accountId: 'ZylatYg', + bidders: ['sovrn', 'pubmatic'], + ids: { maid: ['424', '2982'], hem: 'my-hem' } + } + }] + } +}) +``` + +### Parameters +| Name | Type | Description | Default | +|:-----------------|:----------------------------------------|:-----------------------------------------------------------------------------|:-----------------------| +| name | String | Real time data module name | Always 'experian_rtid' | +| waitForIt | Boolean | Set to true to maximize chance for bidder enrichment, used with auctionDelay | `false` | +| params.accountId | String | Your account id issued by Experian | | +| params.bidders | Array | List of bidders for which you would like data to be set | | +| params.ids | Record or string> | Additional identifiers to send to Experian RTID endpoint | | diff --git a/modules/fabrickIdSystem.js b/modules/fabrickIdSystem.js index dfc2985b0db..bc9c30cb479 100644 --- a/modules/fabrickIdSystem.js +++ b/modules/fabrickIdSystem.js @@ -117,6 +117,12 @@ export const fabrickIdSubmodule = { } catch (e) { logError(`fabrickIdSystem encountered an error`, e); } + }, + eids: { + 'fabrickId': { + source: 'neustar.biz', + atype: 1 + }, } }; diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js index f29ce7508d5..566dd5f40e2 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -4,10 +4,16 @@ */ import { config } from '../src/config.js'; import { getHook } from '../src/hook.js'; -import { getGptSlotForAdUnitCode, logInfo, logWarn } from '../src/utils.js'; +import {deepSetValue, logInfo, logWarn, mergeDeep} from '../src/utils.js'; import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; +import * as events from '../src/events.js' +import CONSTANTS from '../src/constants.json'; +import {currencyCompare} from '../libraries/currencyUtils/currency.js'; +import {maximum, minimum} from '../src/utils/reducers.js'; +import {getGptSlotForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const MODULE = 'fledgeForGpt' +const PENDING = {}; export let isEnabled = false; @@ -20,34 +26,90 @@ export function init(cfg) { if (cfg && cfg.enabled === true) { if (!isEnabled) { getHook('addComponentAuction').before(addComponentAuctionHook); + getHook('makeBidRequests').after(markForFledge); + events.on(CONSTANTS.EVENTS.AUCTION_INIT, onAuctionInit); + events.on(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd); isEnabled = true; } logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} fledge)`, cfg); } else { if (isEnabled) { getHook('addComponentAuction').getHooks({hook: addComponentAuctionHook}).remove(); + getHook('makeBidRequests').getHooks({hook: markForFledge}).remove() + events.off(CONSTANTS.EVENTS.AUCTION_INIT, onAuctionInit); + events.off(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd); isEnabled = false; } logInfo(`${MODULE} disabled`, cfg); } } -export function addComponentAuctionHook(next, adUnitCode, componentAuctionConfig) { - const seller = componentAuctionConfig.seller; +function setComponentAuction(adUnitCode, auctionConfigs) { const gptSlot = getGptSlotForAdUnitCode(adUnitCode); if (gptSlot && gptSlot.setConfig) { gptSlot.setConfig({ - componentAuction: [{ - configKey: seller, - auctionConfig: componentAuctionConfig - }] + componentAuction: auctionConfigs.map(cfg => ({ + configKey: cfg.seller, + auctionConfig: cfg + })) }); - logInfo(MODULE, `register component auction config for: ${adUnitCode} x ${seller}: ${gptSlot.getAdUnitPath()}`, componentAuctionConfig); + logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs); } else { - logWarn(MODULE, `unable to register component auction config for: ${adUnitCode} x ${seller}.`); + logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs); } +} + +function onAuctionInit({auctionId}) { + PENDING[auctionId] = {}; +} + +function getSlotSignals(bidsReceived = [], bidRequests = []) { + let bidfloor, bidfloorcur; + if (bidsReceived.length > 0) { + const bestBid = bidsReceived.reduce(maximum(currencyCompare(bid => [bid.cpm, bid.currency]))); + bidfloor = bestBid.cpm; + bidfloorcur = bestBid.currency; + } else { + const floors = bidRequests.map(bid => typeof bid.getFloor === 'function' && bid.getFloor()).filter(f => f); + const minFloor = floors.length && floors.reduce(minimum(currencyCompare(floor => [floor.floor, floor.currency]))) + bidfloor = minFloor?.floor; + bidfloorcur = minFloor?.currency; + } + const cfg = {}; + if (bidfloor) { + deepSetValue(cfg, 'auctionSignals.prebid.bidfloor', bidfloor); + bidfloorcur && deepSetValue(cfg, 'auctionSignals.prebid.bidfloorcur', bidfloorcur); + } + return cfg; +} + +function onAuctionEnd({auctionId, bidsReceived, bidderRequests}) { + try { + const allReqs = bidderRequests?.flatMap(br => br.bids); + Object.entries(PENDING[auctionId]).forEach(([adUnitCode, auctionConfigs]) => { + const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode; + const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)); + setComponentAuction(adUnitCode, auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg))) + }) + } finally { + delete PENDING[auctionId]; + } +} - next(adUnitCode, componentAuctionConfig); +function setFPDSignals(auctionConfig, fpd) { + auctionConfig.auctionSignals = mergeDeep({}, {prebid: fpd}, auctionConfig.auctionSignals); +} + +export function addComponentAuctionHook(next, request, componentAuctionConfig) { + const {adUnitCode, auctionId, ortb2, ortb2Imp} = request; + if (PENDING.hasOwnProperty(auctionId)) { + setFPDSignals(componentAuctionConfig, {ortb2, ortb2Imp}); + !PENDING[auctionId].hasOwnProperty(adUnitCode) && (PENDING[auctionId][adUnitCode] = []); + PENDING[auctionId][adUnitCode].push(componentAuctionConfig); + } else { + logWarn(MODULE, `Received component auction config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, componentAuctionConfig) + } + next(request, componentAuctionConfig); } function isFledgeSupported() { @@ -56,16 +118,23 @@ function isFledgeSupported() { export function markForFledge(next, bidderRequests) { if (isFledgeSupported()) { - bidderRequests.forEach((req) => { - req.fledgeEnabled = config.runWithBidder(req.bidderCode, () => config.getConfig('fledgeEnabled')) - }) + const globalFledgeConfig = config.getConfig('fledgeForGpt'); + const bidders = globalFledgeConfig?.bidders ?? []; + bidderRequests.forEach((bidderReq) => { + const useGlobalConfig = globalFledgeConfig?.enabled && (bidders.length === 0 || bidders.includes(bidderReq.bidderCode)); + config.runWithBidder(bidderReq.bidderCode, () => { + const fledgeEnabled = config.getConfig('fledgeEnabled') ?? (useGlobalConfig ? globalFledgeConfig.enabled : undefined); + const defaultForSlots = config.getConfig('defaultForSlots') ?? (useGlobalConfig ? globalFledgeConfig?.defaultForSlots : undefined); + Object.assign(bidderReq, {fledgeEnabled}); + bidderReq.bids.forEach(bidReq => { deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidReq.ortb2Imp?.ext?.ae ?? defaultForSlots) }) + }) + }); } next(bidderRequests); } -getHook('makeBidRequests').after(markForFledge); export function setImpExtAe(imp, bidRequest, context) { - if (!context.bidderRequest.fledgeEnabled) { + if (imp.ext?.ae && !context.bidderRequest.fledgeEnabled) { delete imp.ext?.ae; } } diff --git a/modules/fledgeForGpt.md b/modules/fledgeForGpt.md index 3bb86cd5946..28f44da6459 100644 --- a/modules/fledgeForGpt.md +++ b/modules/fledgeForGpt.md @@ -15,8 +15,8 @@ This is accomplished by adding the `fledgeForGpt` module to the list of modules gulp build --modules=fledgeForGpt,... ``` -Second, they must enable FLEDGE in their Prebid.js configuration. To provide a high degree of flexiblity for testing, FLEDGE -settings exist at the module level, the bidder level, and the slot level. +Second, they must enable FLEDGE in their Prebid.js configuration. +This is done through module level configuration, but to provide a high degree of flexiblity for testing, FLEDGE settings also exist at the bidder level and slot level. ### Module Configuration This module exposes the following settings: @@ -24,15 +24,20 @@ This module exposes the following settings: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | |enabled | Boolean |Enable/disable the module |Defaults to `false` | +|bidders | Array[String] |Optional list of bidders |Defaults to all bidders | +|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1 | -As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module -using the `setConfig` method of Prebid.js: +As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module and configure `defaultForSlots` to be `1` (meaning _Client-side auction_). +using the `setConfig` method of Prebid.js. Optionally, a list of +bidders to apply these settings to may be provided: ```js pbjs.que.push(function() { pbjs.setConfig({ fledgeForGpt: { - enabled: true + enabled: true, + bidders: ['openx', 'rtbhouse'], + defaultForSlots: 1 } }); }); @@ -44,23 +49,25 @@ This module adds the following setting for bidders: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | | fledgeEnabled | Boolean | Enable/disable a bidder to participate in FLEDGE | Defaults to `false` | +|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1| -In addition to enabling FLEDGE at the module level, individual bidders must also be enabled. This allows publishers to -selectively test with one or more bidders as they desire. To enable one or more bidders, use the `setBidderConfig` method +Individual bidders may be further included or excluded here using the `setBidderConfig` method of Prebid.js: ```js pbjs.setBidderConfig({ bidders: ["openx"], config: { - fledgeEnabled: true + fledgeEnabled: true, + defaultForSlots: 1 } }); ``` ### AdUnit Configuration -Enabling an adunit for FLEDGE eligibility is accomplished by setting an attribute of the `ortb2Imp` object for that -adunit. +All adunits can be opted-in to FLEDGE in the global config via the `defaultForSlots` parameter. +If needed, adunits can be configured individually by setting an attribute of the `ortb2Imp` object for that +adunit. This attribute will take precedence over `defaultForSlots` setting. |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | diff --git a/modules/flippBidAdapter.js b/modules/flippBidAdapter.js new file mode 100644 index 00000000000..480e414992d --- /dev/null +++ b/modules/flippBidAdapter.js @@ -0,0 +1,186 @@ +import {isEmpty, parseUrl} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; + +const NETWORK_ID = 11090; +const AD_TYPES = [4309, 641]; +const DTX_TYPES = [5061]; +const TARGET_NAME = 'inline'; +const BIDDER_CODE = 'flipp'; +const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding'; +const DEFAULT_TTL = 30; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_CREATIVE_TYPE = 'NativeX'; +const VALID_CREATIVE_TYPES = ['DTX', 'NativeX']; +const FLIPP_USER_KEY = 'flipp-uid'; +const COMPACT_DEFAULT_HEIGHT = 600; + +let userKey = null; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +export function getUserKey(options = {}) { + if (userKey) { + return userKey; + } + + // If the partner provides the user key use it, otherwise fallback to cookies + if ('userKey' in options && options.userKey) { + if (isValidUserKey(options.userKey)) { + userKey = options.userKey; + return options.userKey; + } + } + + // Grab from Cookie + const foundUserKey = storage.cookiesAreEnabled(null) && storage.getCookie(FLIPP_USER_KEY, null); + if (foundUserKey && isValidUserKey(foundUserKey)) { + return foundUserKey; + } + + // Generate if none found + userKey = generateUUID(); + + // Set cookie + if (storage.cookiesAreEnabled()) { + storage.setCookie(FLIPP_USER_KEY, userKey); + } + + return userKey; +} + +function isValidUserKey(userKey) { + return typeof userKey === 'string' && !userKey.startsWith('#') && userKey.length > 0; +} + +const generateUUID = () => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +}; + +/** + * Determines if a creativeType is valid + * + * @param {string} creativeType The Creative Type to validate. + * @return string creativeType if this is a valid Creative Type, and 'NativeX' otherwise. + */ +const validateCreativeType = (creativeType) => { + if (creativeType && VALID_CREATIVE_TYPES.includes(creativeType)) { + return creativeType; + } else { + return DEFAULT_CREATIVE_TYPE; + } +}; + +const getAdTypes = (creativeType) => { + if (creativeType === 'DTX') { + return DTX_TYPES; + } + return AD_TYPES; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!(bid.params.siteId) && !!(bid.params.publisherNameIdentifier); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests[] an array of bids + * @param {BidderRequest} bidderRequest master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const urlParams = parseUrl(bidderRequest.refererInfo.page).search; + const contentCode = urlParams['flipp-content-code']; + const userKey = getUserKey(validBidRequests[0]?.params); + const placements = validBidRequests.map((bid, index) => { + const options = bid.params.options || {}; + if (!options.hasOwnProperty('startCompact')) { + options.startCompact = true; + } + return { + divName: TARGET_NAME, + networkId: NETWORK_ID, + siteId: bid.params.siteId, + adTypes: getAdTypes(bid.params.creativeType), + count: 1, + ...(!isEmpty(bid.params.zoneIds) && {zoneIds: bid.params.zoneIds}), + properties: { + ...(!isEmpty(contentCode) && {contentCode: contentCode.slice(0, 32)}), + }, + options, + prebid: { + requestId: bid.bidId, + publisherNameIdentifier: bid.params.publisherNameIdentifier, + height: bid.mediaTypes.banner.sizes[index][0], + width: bid.mediaTypes.banner.sizes[index][1], + creativeType: validateCreativeType(bid.params.creativeType), + } + } + }); + return { + method: 'POST', + url: ENDPOINT, + data: { + placements, + url: bidderRequest.refererInfo.page, + user: { + key: userKey, + }, + }, + } + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest A bid request object + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + if (!serverResponse?.body) return []; + const placements = bidRequest.data.placements; + const res = serverResponse.body; + if (!isEmpty(res) && !isEmpty(res.decisions) && !isEmpty(res.decisions.inline)) { + return res.decisions.inline.map(decision => { + const placement = placements.find(p => p.prebid.requestId === decision.prebid?.requestId); + const height = placement.options?.startCompact ? COMPACT_DEFAULT_HEIGHT : decision.height; + return { + bidderCode: BIDDER_CODE, + requestId: decision.prebid?.requestId, + cpm: decision.prebid?.cpm, + width: decision.width, + height, + creativeId: decision.adId, + currency: DEFAULT_CURRENCY, + netRevenue: true, + ttl: DEFAULT_TTL, + ad: decision.prebid?.creative, + } + }); + } + return []; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: (syncOptions, serverResponses) => [], +} +registerBidder(spec); diff --git a/modules/flippBidAdapter.md b/modules/flippBidAdapter.md new file mode 100644 index 00000000000..e823432a60f --- /dev/null +++ b/modules/flippBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: Flipp Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@flipp.com +``` + +# Description + +This module connects publishers to Flipp's Shopper Experience via Prebid.js. + + +# Test parameters + +```javascript +var adUnits = [ + { + code: 'flipp-scroll-ad-content', + mediaTypes: { + banner: { + sizes: [ + [300, 600] + ] + } + }, + bids: [ + { + bidder: 'flipp', + params: { + creativeType: 'NativeX', // Optional, can be one of 'NativeX' (default) or 'DTX' + publisherNameIdentifier: 'wishabi-test-publisher', // Required + siteId: 1192075, // Required + zoneIds: [260678], // Optional + userKey: ``, // Optional, but recommended for better user experience. Can be a cookie, session id or any other user identifier + options: { + startCompact: true, // Optional. Height of the experience will be reduced. Default to true + dwellExpand: true // Optional. Auto expand the experience after a certain time passes. Default to true + } + } + } + ] + } +] +``` diff --git a/modules/fluctBidAdapter.js b/modules/fluctBidAdapter.js index edb750a6b90..b566769c00e 100644 --- a/modules/fluctBidAdapter.js +++ b/modules/fluctBidAdapter.js @@ -8,16 +8,6 @@ const VERSION = '1.2'; const NET_REVENUE = true; const TTL = 300; -/** - * See modules/userId/eids.js for supported sources - */ -const SUPPORTED_USER_ID_SOURCES = [ - 'adserver.org', - 'criteo.com', - 'intimatemerger.com', - 'liveramp.com', -]; - export const spec = { code: BIDDER_CODE, aliases: ['adingo'], @@ -36,6 +26,7 @@ export const spec = { * Make a server request from the list of BidRequests. * * @param {validBidRequests[]} - an array of bids. + * @param {bidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { @@ -43,16 +34,24 @@ export const spec = { const page = bidderRequest.refererInfo.page; _each(validBidRequests, (request) => { + const impExt = request.ortb2Imp?.ext; const data = Object(); data.page = page; data.adUnitCode = request.adUnitCode; data.bidId = request.bidId; - data.transactionId = request.ortb2Imp?.ext?.tid; data.user = { - eids: (request.userIdAsEids || []).filter((eid) => SUPPORTED_USER_ID_SOURCES.indexOf(eid.source) !== -1) + data: bidderRequest.ortb2?.user?.data ?? [], + eids: [ + ...(request.userIdAsEids ?? []), + ...(bidderRequest.ortb2?.user?.ext?.eids ?? []), + ], }; + if (impExt) { + data.transactionId = impExt.tid; + data.gpid = impExt.gpid ?? impExt.data?.pbadslot ?? impExt.data?.adserver?.adslot; + } if (bidderRequest.gdprConsent) { deepSetValue(data, 'regs.gdpr', { consent: bidderRequest.gdprConsent.consentString, diff --git a/modules/freepassBidAdapter.js b/modules/freepassBidAdapter.js new file mode 100644 index 00000000000..cdcc3c6a4b0 --- /dev/null +++ b/modules/freepassBidAdapter.js @@ -0,0 +1,117 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {logMessage} from '../src/utils.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +const BIDDER_SERVICE_URL = 'https://bidding-dsp.ad-m.asia/dsp/api/bid/s/s/freepass'; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + } +}); + +function prepareUserInfo(user, freepassId) { + let userInfo = user || {}; + let extendedUserInfo = userInfo.ext || {}; + + if (freepassId.userId) { + userInfo.id = freepassId.userId; + } + + if (freepassId.commonId) { + extendedUserInfo.fuid = freepassId.commonId; + } + userInfo.ext = extendedUserInfo; + + return userInfo; +} + +function prepareDeviceInfo(device, freepassId) { + let deviceInfo = device || {}; + let extendedDeviceInfo = deviceInfo.ext || {}; + + extendedDeviceInfo.is_accurate_ip = 0; + if (freepassId.userIp) { + deviceInfo.ip = freepassId.userIp; + extendedDeviceInfo.is_accurate_ip = 1; + } + deviceInfo.ext = extendedDeviceInfo; + + return deviceInfo; +} + +export const spec = { + code: 'freepass', + supportedMediaTypes: [BANNER], + + isBidRequestValid(bid) { + logMessage('Validating bid: ', bid); + return !(!bid.adUnitCode || !bid.params || !bid.params.publisherId); + }, + + buildRequests(validBidRequests, bidderRequest) { + if (validBidRequests.length === 0) { + logMessage('FreePass BidAdapter has no valid bid requests'); + return []; + } + + logMessage('FreePass BidAdapter is preparing bid request: ', validBidRequests); + logMessage('FreePass BidAdapter is using bidder request: ', bidderRequest); + + const data = converter.toORTB({ + bidderRequest: bidderRequest, + bidRequests: validBidRequests, + context: { mediaType: BANNER } + }); + logMessage('FreePass BidAdapter interpreted ORTB bid request as ', data); + + // Only freepassId is supported + let freepassId = (validBidRequests[0].userId && validBidRequests[0].userId.freepassId) || {}; + data.user = prepareUserInfo(data.user, freepassId); + data.device = prepareDeviceInfo(data.device, freepassId); + + // set site.page & site.publisher + data.site = data.site || {}; + data.site.publisher = data.site.publisher || {}; + // set site.publisher.id. from params.publisherId required + data.site.publisher.id = validBidRequests[0].params.publisherId; + // set site.publisher.domain from params.publisherUrl. optional + data.site.publisher.domain = validBidRequests[0].params?.publisherUrl; + + // set source + data.source = data.source || {}; + data.source.fd = 0; + data.source.tid = validBidRequests.ortb2?.source?.tid; + data.source.pchain = ''; + + // set imp.ext + validBidRequests.forEach((bidRequest, index) => { + data.imp[index].tagId = bidRequest.adUnitCode; + }); + + data.test = validBidRequests[0].test || 0; + + logMessage('FreePass BidAdapter augmented ORTB bid request user: ', data.user); + logMessage('FreePass BidAdapter augmented ORTB bid request device: ', data.device); + + return { + method: 'POST', + url: BIDDER_SERVICE_URL, + data, + options: { withCredentials: false } + }; + }, + + interpretResponse(serverResponse, bidRequest) { + logMessage('FreePass BidAdapter is interpreting server response: ', serverResponse); + logMessage('FreePass BidAdapter is using bid request: ', bidRequest); + const bids = converter.fromORTB({response: serverResponse.body, request: bidRequest.data}).bids; + logMessage('FreePass BidAdapter interpreted ORTB bids as ', bids); + + return bids; + }, +}; + +registerBidder(spec); diff --git a/modules/freepassBidAdapter.md b/modules/freepassBidAdapter.md new file mode 100644 index 00000000000..7b56a469583 --- /dev/null +++ b/modules/freepassBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: FreePass Bidder Adapter +Module Type: Bidder Adapter +Maintainer: fp-hbidding@freebit.net +``` + +# Description + +Connects to FreePass service for bids. Only BANNER is currently supported. + +This BidAdapter requires the FreePass IdSystem to be configured. Please contact FreePass for proper setup. + +# Test Parameters +```javascript + let adUnits = [ + { + code: 'ad-banner-1', // ad slot HTML element ID + mediaTypes: { + banner: { + sizes: [[1024, 1024]] + } + }, + bids: [{ + bidder: 'freepass', + params: { + publisherId: '12345' + } + }] + } + ]; +``` + diff --git a/modules/freepassIdSystem.js b/modules/freepassIdSystem.js index d52c537e800..419aa9ec414 100644 --- a/modules/freepassIdSystem.js +++ b/modules/freepassIdSystem.js @@ -1,8 +1,12 @@ import { submodule } from '../src/hook.js'; -import {generateUUID, logMessage} from '../src/utils.js'; +import { logMessage } from '../src/utils.js'; +import { getCoreStorageManager } from '../src/storageManager.js'; const MODULE_NAME = 'freepassId'; +export const FREEPASS_COOKIE_KEY = '_f_UF8cCRlr'; +export const storage = getCoreStorageManager(MODULE_NAME); + export const freepassIdSubmodule = { name: MODULE_NAME, decode: function (value, config) { @@ -15,7 +19,12 @@ export const freepassIdSubmodule = { logMessage('Getting FreePass ID using config: ' + JSON.stringify(config)); const freepassData = config.params !== undefined ? (config.params.freepassData || {}) : {} - let idObject = {userId: generateUUID()}; + const idObject = {}; + + const userId = storage.getCookie(FREEPASS_COOKIE_KEY); + if (userId !== null) { + idObject.userId = userId; + } if (freepassData.commonId !== undefined) { idObject.commonId = config.params.freepassData.commonId; @@ -29,8 +38,8 @@ export const freepassIdSubmodule = { }, extendId: function (config, consent, cachedIdObject) { - let freepassData = config.params.freepassData; - let hasFreepassData = freepassData !== undefined; + const freepassData = config.params.freepassData; + const hasFreepassData = freepassData !== undefined; if (!hasFreepassData) { logMessage('No Freepass Data. CachedIdObject will not be extended: ' + JSON.stringify(cachedIdObject)); return { @@ -38,12 +47,7 @@ export const freepassIdSubmodule = { }; } - if (freepassData.commonId === cachedIdObject.commonId && freepassData.userIp === cachedIdObject.userIp) { - logMessage('FreePass ID is already up-to-date: ' + JSON.stringify(cachedIdObject)); - return { - id: cachedIdObject - }; - } + const currentCookieId = storage.getCookie(FREEPASS_COOKIE_KEY); logMessage('Extending FreePass ID object: ' + JSON.stringify(cachedIdObject)); logMessage('Extending FreePass ID using config: ' + JSON.stringify(config)); @@ -52,8 +56,8 @@ export const freepassIdSubmodule = { id: { commonId: freepassData.commonId, userIp: freepassData.userIp, - userId: cachedIdObject.userId, - }, + userId: currentCookieId + } }; } }; diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index cd4785cdc78..ba819abd687 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -4,6 +4,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; const BIDDER_CODE = 'freewheel-ssp'; +const GVL_ID = 285; const PROTOCOL = getProtocol(); const FREEWHEEL_ADSSETUP = PROTOCOL + '://ads.stickyadstv.com/www/delivery/swfIndex.php'; @@ -314,6 +315,7 @@ var getOutstreamScript = function(bid) { export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], aliases: ['stickyadstv', 'freewheelssp'], // aliases for freewheel-ssp /** @@ -382,6 +384,15 @@ export const spec = { requestParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; } + // Add content object + if (bidderRequest && bidderRequest.ortb2 && bidderRequest.ortb2.site && bidderRequest.ortb2.site.content && typeof bidderRequest.ortb2.site.content === 'object') { + try { + requestParams._fw_prebid_content = JSON.stringify(bidderRequest.ortb2.site.content); + } catch (error) { + logWarn('PREBID - ' + BIDDER_CODE + ': Unable to stringify the content object: ' + error); + } + } + // Add schain object var schain = currentBidRequest.schain; if (schain) { @@ -532,10 +543,11 @@ export const spec = { }; if (bidrequest.mediaTypes.video) { - bidResponse.vastXml = serverResponse; bidResponse.mediaType = 'video'; } + bidResponse.vastXml = serverResponse; + bidResponse.ad = formatAdHTML(bidrequest, playerSize); bidResponses.push(bidResponse); } diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index 5f09a315b34..809f1311c42 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -221,6 +221,22 @@ export const ftrackIdSubmodule = { if (usPrivacyVersion == 1 && usPrivacyOptOutSale === 'Y') consentValue = false; return consentValue; + }, + eids: { + 'ftrackId': { + source: 'flashtalking.com', + atype: 1, + getValue: function(data) { + let value = ''; + if (data && data.ext && data.ext.DeviceID) { + value = data.ext.DeviceID; + } + return value; + }, + getUidExt: function(data) { + return data && data.ext; + } + }, } }; diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 4bac4b0cf74..4e600f71b90 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -5,7 +5,6 @@ import {deepAccess, logError, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {gdprDataHandler} from '../src/adapterManager.js'; -import {find} from '../src/polyfill.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js'; @@ -24,44 +23,65 @@ import { import {registerActivityControl} from '../src/activities/rules.js'; import { ACTIVITY_ACCESS_DEVICE, - ACTIVITY_ENRICH_EIDS, + ACTIVITY_ENRICH_EIDS, ACTIVITY_ENRICH_UFPD, ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS, - ACTIVITY_SYNC_USER + ACTIVITY_SYNC_USER, ACTIVITY_TRANSMIT_EIDS, ACTIVITY_TRANSMIT_PRECISE_GEO, ACTIVITY_TRANSMIT_UFPD } from '../src/activities/activities.js'; export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; -const TCF2 = { - 'purpose1': {id: 1, name: 'storage'}, - 'purpose2': {id: 2, name: 'basicAds'}, - 'purpose7': {id: 7, name: 'measurement'} +export const ACTIVE_RULES = { + purpose: {}, + feature: {} }; -/* - These rules would be used if `consentManagement.gdpr.rules` is undefined by the publisher. -*/ -const DEFAULT_RULES = [{ - purpose: 'storage', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] -}, { - purpose: 'basicAds', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] -}]; - -export let purpose1Rule; -export let purpose2Rule; -export let purpose7Rule; - -export let enforcementRules; +const CONSENT_PATHS = { + purpose: 'purpose.consents', + feature: 'specialFeatureOptins' +}; + +const CONFIGURABLE_RULES = { + storage: { + type: 'purpose', + default: { + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + }, + id: 1, + }, + basicAds: { + type: 'purpose', + id: 2, + default: { + purpose: 'basicAds', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + } + }, + personalizedAds: { + type: 'purpose', + id: 4, + }, + measurement: { + type: 'purpose', + id: 7, + }, + transmitPreciseGeo: { + type: 'feature', + id: 1, + }, +}; const storageBlocked = new Set(); const biddersBlocked = new Set(); const analyticsBlocked = new Set(); +const ufpdBlocked = new Set(); +const eidsBlocked = new Set(); +const geoBlocked = new Set(); let hooksAdded = false; let strictStorageEnforcement = false; @@ -76,6 +96,9 @@ const GVLID_LOOKUP_PRIORITY = [ const RULE_NAME = 'TCF2'; const RULE_HANDLES = []; +// in JS we do not have access to the GVL; assume that everyone declares legitimate interest for basic ads +const LI_PURPOSES = [2]; + /** * Retrieve a module's GVL ID. */ @@ -140,6 +163,16 @@ export function shouldEnforce(consentData, purpose, name) { return consentData && consentData.gdprApplies; } +function getConsent(consentData, type, id, gvlId) { + let purpose = !!deepAccess(consentData, `vendorData.${CONSENT_PATHS[type]}.${id}`); + let vendor = !!deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`); + if (type === 'purpose' && LI_PURPOSES.includes(id)) { + purpose ||= !!deepAccess(consentData, `vendorData.purpose.legitimateInterests.${id}`); + vendor ||= !!deepAccess(consentData, `vendorData.vendor.legitimateInterests.${gvlId}`); + } + return {purpose, vendor}; +} + /** * This function takes in a rule and consentData and validates against the consentData provided. Depending on what it returns, * the caller may decide to suppress a TCF-sensitive activity. @@ -150,55 +183,24 @@ export function shouldEnforce(consentData, purpose, name) { * @returns {boolean} */ export function validateRules(rule, consentData, currentModule, gvlId) { - const purposeId = TCF2[Object.keys(TCF2).filter(purposeName => TCF2[purposeName].name === rule.purpose)[0]].id; + const ruleOptions = CONFIGURABLE_RULES[rule.purpose]; // return 'true' if vendor present in 'vendorExceptions' if ((rule.vendorExceptions || []).includes(currentModule)) { return true; } - const vendorConsentRequred = !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule))); - - // get data from the consent string - const purposeConsent = deepAccess(consentData, `vendorData.purpose.consents.${purposeId}`); - const vendorConsent = vendorConsentRequred ? deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`) : true; - const liTransparency = deepAccess(consentData, `vendorData.purpose.legitimateInterests.${purposeId}`); - - /* - Since vendor exceptions have already been handled, the purpose as a whole is allowed if it's not being enforced - or the user has consented. Similar with vendors. - */ - const purposeAllowed = rule.enforcePurpose === false || purposeConsent === true; - const vendorAllowed = rule.enforceVendor === false || vendorConsent === true; - - /* - Few if any vendors should be declaring Legitimate Interest for Device Access (Purpose 1), but some are claiming - LI for Basic Ads (Purpose 2). Prebid.js can't check to see who's declaring what legal basis, so if LI has been - established for Purpose 2, allow the auction to take place and let the server sort out the legal basis calculation. - */ - if (purposeId === 2) { - return (purposeAllowed && vendorAllowed) || (liTransparency === true); - } - - return purposeAllowed && vendorAllowed; + const vendorConsentRequred = rule.enforceVendor && !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule))); + const {purpose, vendor} = getConsent(consentData, ruleOptions.type, ruleOptions.id, gvlId); + return (!rule.enforcePurpose || purpose) && (!vendorConsentRequred || vendor); } -/** - * all activity rules follow the same structure: - * if GDPR is in scope, check configuration for a particular purpose, and if that enables enforcement, - * check against consent data for that purpose and vendor - * - * @param purposeNo TCF purpose number to check for this activity - * @param getEnforcementRule getter for gdprEnforcement rule definition to use - * @param blocked optional set to use for collecting denied vendors - * @param gvlidFallback optional factory function for a gvlid falllback function - */ -function gdprRule(purposeNo, getEnforcementRule, blocked = null, gvlidFallback = () => null) { +function gdprRule(purposeNo, checkConsent, blocked = null, gvlidFallback = () => null) { return function (params) { const consentData = gdprDataHandler.getConsentData(); const modName = params[ACTIVITY_PARAM_COMPONENT_NAME]; if (shouldEnforce(consentData, purposeNo, modName)) { const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], modName, gvlidFallback(params)); - let allow = !!validateRules(getEnforcementRule(), consentData, modName, gvlid); + let allow = !!checkConsent(consentData, modName, gvlid); if (!allow) { blocked && blocked.add(modName); return {allow}; @@ -207,30 +209,62 @@ function gdprRule(purposeNo, getEnforcementRule, blocked = null, gvlidFallback = }; } -export const accessDeviceRule = ((rule) => { - return function (params) { - // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set - if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID && !strictStorageEnforcement) return; - return rule(params); - }; -})(gdprRule(1, () => purpose1Rule, storageBlocked)); - -export const syncUserRule = gdprRule(1, () => purpose1Rule, storageBlocked); -export const enrichEidsRule = gdprRule(1, () => purpose1Rule, storageBlocked); +function singlePurposeGdprRule(purposeNo, blocked = null, gvlidFallback = () => null) { + return gdprRule(purposeNo, (cd, modName, gvlid) => !!validateRules(ACTIVE_RULES.purpose[purposeNo], cd, modName, gvlid), blocked, gvlidFallback); +} -export const fetchBidsRule = ((rule) => { +function exceptPrebidModules(ruleFn) { return function (params) { - if (params[ACTIVITY_PARAM_COMPONENT_TYPE] !== MODULE_TYPE_BIDDER) { + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID) { // TODO: this special case is for the PBS adapter (componentType is 'prebid') // we should check for generic purpose 2 consent & vendor consent based on the PBS vendor's GVL ID; // that is, however, a breaking change and skipped for now return; } + return ruleFn(params); + }; +} + +export const accessDeviceRule = ((rule) => { + return function (params) { + // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID && !strictStorageEnforcement) return; return rule(params); }; -})(gdprRule(2, () => purpose2Rule, biddersBlocked)); +})(singlePurposeGdprRule(1, storageBlocked)); + +export const syncUserRule = singlePurposeGdprRule(1, storageBlocked); +export const enrichEidsRule = singlePurposeGdprRule(1, storageBlocked); +export const fetchBidsRule = exceptPrebidModules(singlePurposeGdprRule(2, biddersBlocked)); +export const reportAnalyticsRule = singlePurposeGdprRule(7, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG])); +export const ufpdRule = singlePurposeGdprRule(4, ufpdBlocked); + +export const transmitEidsRule = exceptPrebidModules((() => { + // Transmit EID special case: + // by default, legal basis or vendor exceptions for any purpose between 2 and 10 + // (but disregarding enforcePurpose and enforceVendor config) is enough to allow EIDs through + function check2to10Consent(consentData, modName, gvlId) { + for (let pno = 2; pno <= 10; pno++) { + if (ACTIVE_RULES.purpose[pno]?.vendorExceptions?.includes(modName)) { + return true; + } + const {purpose, vendor} = getConsent(consentData, 'purpose', pno, gvlId); + if (purpose && (vendor || ACTIVE_RULES.purpose[pno]?.softVendorExceptions?.includes(modName))) { + return true; + } + } + return false; + } + + const defaultBehavior = gdprRule('2-10', check2to10Consent, eidsBlocked); + const p4Behavior = singlePurposeGdprRule(4, eidsBlocked); + return function () { + const fn = ACTIVE_RULES.purpose[4]?.eidsRequireP4Consent ? p4Behavior : defaultBehavior; + return fn.apply(this, arguments); + }; +})()); -export const reportAnalyticsRule = gdprRule(7, () => purpose7Rule, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG])); +export const transmitPreciseGeoRule = gdprRule('Special Feature 1', (cd, modName, gvlId) => validateRules(ACTIVE_RULES.feature[1], cd, modName, gvlId), geoBlocked); /** * Compiles the TCF2.0 enforcement results into an object, which is emitted as an event payload to "tcf2Enforcement" event. @@ -243,67 +277,57 @@ function emitTCF2FinalResults() { const tcf2FinalResults = { storageBlocked: formatSet(storageBlocked), biddersBlocked: formatSet(biddersBlocked), - analyticsBlocked: formatSet(analyticsBlocked) + analyticsBlocked: formatSet(analyticsBlocked), + ufpdBlocked: formatSet(ufpdBlocked), + eidsBlocked: formatSet(eidsBlocked), + geoBlocked: formatSet(geoBlocked) }; events.emit(CONSTANTS.EVENTS.TCF2_ENFORCEMENT, tcf2FinalResults); - [storageBlocked, biddersBlocked, analyticsBlocked].forEach(el => el.clear()); + [storageBlocked, biddersBlocked, analyticsBlocked, ufpdBlocked, eidsBlocked, geoBlocked].forEach(el => el.clear()); } events.on(CONSTANTS.EVENTS.AUCTION_END, emitTCF2FinalResults); -/* - Set of callback functions used to detect presence of a TCF rule, passed as the second argument to find(). -*/ -const hasPurpose1 = (rule) => { - return rule.purpose === TCF2.purpose1.name; -}; -const hasPurpose2 = (rule) => { - return rule.purpose === TCF2.purpose2.name; -}; -const hasPurpose7 = (rule) => { - return rule.purpose === TCF2.purpose7.name; -}; - /** * A configuration function that initializes some module variables, as well as adds hooks * @param {Object} config - GDPR enforcement config object */ export function setEnforcementConfig(config) { - const rules = deepAccess(config, 'gdpr.rules'); + let rules = deepAccess(config, 'gdpr.rules'); if (!rules) { logWarn('TCF2: enforcing P1 and P2 by default'); - enforcementRules = DEFAULT_RULES; - } else { - enforcementRules = rules; } + rules = Object.fromEntries((rules || []).map(r => [r.purpose, r])); strictStorageEnforcement = !!deepAccess(config, STRICT_STORAGE_ENFORCEMENT); - purpose1Rule = find(enforcementRules, hasPurpose1); - purpose2Rule = find(enforcementRules, hasPurpose2); - purpose7Rule = find(enforcementRules, hasPurpose7); - - if (!purpose1Rule) { - purpose1Rule = DEFAULT_RULES[0]; - } - - if (!purpose2Rule) { - purpose2Rule = DEFAULT_RULES[1]; - } + Object.entries(CONFIGURABLE_RULES).forEach(([name, opts]) => { + ACTIVE_RULES[opts.type][opts.id] = rules[name] ?? opts.default; + }); if (!hooksAdded) { - if (purpose1Rule) { + if (ACTIVE_RULES.purpose[1] != null) { hooksAdded = true; RULE_HANDLES.push(registerActivityControl(ACTIVITY_ACCESS_DEVICE, RULE_NAME, accessDeviceRule)); RULE_HANDLES.push(registerActivityControl(ACTIVITY_SYNC_USER, RULE_NAME, syncUserRule)); RULE_HANDLES.push(registerActivityControl(ACTIVITY_ENRICH_EIDS, RULE_NAME, enrichEidsRule)); } - if (purpose2Rule) { + if (ACTIVE_RULES.purpose[2] != null) { RULE_HANDLES.push(registerActivityControl(ACTIVITY_FETCH_BIDS, RULE_NAME, fetchBidsRule)); } - if (purpose7Rule) { + if (ACTIVE_RULES.purpose[4] != null) { + RULE_HANDLES.push( + registerActivityControl(ACTIVITY_TRANSMIT_UFPD, RULE_NAME, ufpdRule), + registerActivityControl(ACTIVITY_ENRICH_UFPD, RULE_NAME, ufpdRule) + ); + } + if (ACTIVE_RULES.purpose[7] != null) { RULE_HANDLES.push(registerActivityControl(ACTIVITY_REPORT_ANALYTICS, RULE_NAME, reportAnalyticsRule)); } + if (ACTIVE_RULES.feature[1] != null) { + RULE_HANDLES.push(registerActivityControl(ACTIVITY_TRANSMIT_PRECISE_GEO, RULE_NAME, transmitPreciseGeoRule)); + } + RULE_HANDLES.push(registerActivityControl(ACTIVITY_TRANSMIT_EIDS, RULE_NAME, transmitEidsRule)); } } diff --git a/modules/geoedgeRtdProvider.js b/modules/geoedgeRtdProvider.js index 6f910632fbc..646d2f4e786 100644 --- a/modules/geoedgeRtdProvider.js +++ b/modules/geoedgeRtdProvider.js @@ -20,6 +20,8 @@ import { ajax } from '../src/ajax.js'; import { generateUUID, insertElement, isEmpty, logError } from '../src/utils.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; +import { loadExternalScript } from '../src/adloader.js'; +import { auctionManager } from '../src/auctionManager.js'; /** @type {string} */ const SUBMODULE_NAME = 'geoedge'; @@ -33,9 +35,13 @@ const PV_ID = generateUUID(); /** @type {string} */ const HOST_NAME = 'https://rumcdn.geoedge.be'; /** @type {string} */ -const FILE_NAME = 'grumi.js'; +const FILE_NAME_CLIENT = 'grumi.js'; +/** @type {string} */ +const FILE_NAME_INPAGE = 'grumi-ip.js'; +/** @type {function} */ +export let getClientUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME_CLIENT}`; /** @type {function} */ -export let getClientUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME}`; +export let getInPageUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME_INPAGE}`; /** @type {string} */ export let wrapper /** @type {boolean} */; @@ -177,7 +183,8 @@ function isSupportedBidder(bidder, paramsBidders) { function shouldWrap(bid, params) { let supportedBidder = isSupportedBidder(bid.bidderCode, params.bidders); let donePreload = params.wap ? preloaded : true; - return wrapperReady && supportedBidder && donePreload; + let isGPT = params.gpt; + return wrapperReady && supportedBidder && donePreload && !isGPT; } function conditionallyWrap(bidResponse, config, userConsent) { @@ -187,31 +194,55 @@ function conditionallyWrap(bidResponse, config, userConsent) { } } +function isBillingMessage(data, params) { + return data.key === params.key && data.impression; +} + /** - * Fire billable events for applicable bids + * Fire billable events when our client sends a message + * Messages will be sent only when: + * a. applicable bids are wrapped + * b. our code laoded and executed sucesfully */ function fireBillableEventsForApplicableBids(params) { - events.on(CONSTANTS.EVENTS.BID_WON, function (winningBid) { - if (shouldWrap(winningBid, params)) { + window.addEventListener('message', function (message) { + let data = message.data; + if (isBillingMessage(data, params)) { + let winningBid = auctionManager.findBidByAdId(data.adId); events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { vendor: SUBMODULE_NAME, - billingId: generateUUID(), - type: 'impression', - transactionId: winningBid.transactionId, - auctionId: winningBid.auctionId, - bidId: winningBid.requestId + billingId: data.impressionId, + type: winningBid ? 'impression' : data.type, + transactionId: winningBid?.transactionId || data.transactionId, + auctionId: winningBid?.auctionId || data.auctionId, + bidId: winningBid?.requestId || data.requestId }); } }); } +/** + * Loads Geoedge in page script that monitors all ad slots created by GPT + * @param {Object} params + */ +function setupInPage(params) { + window.grumi = params; + window.grumi.fromPrebid = true; + loadExternalScript(getInPageUrl(params.key), SUBMODULE_NAME); +} + function init(config, userConsent) { let params = config.params; if (!params || !params.key) { logError('missing key for geoedge RTD module provider'); return false; } - preloadClient(params.key); + if (params.gpt) { + setupInPage(params); + } else { + fetchWrapper(setWrapper); + preloadClient(params.key); + } fireBillableEventsForApplicableBids(params); return true; } @@ -227,9 +258,4 @@ export const geoedgeSubmodule = { onBidResponseEvent: conditionallyWrap }; -export function beforeInit() { - fetchWrapper(setWrapper); - submodule('realTimeData', geoedgeSubmodule); -} - -beforeInit(); +submodule('realTimeData', geoedgeSubmodule); diff --git a/modules/geoedgeRtdProvider.md b/modules/geoedgeRtdProvider.md index 5414606612c..cdf913b8893 100644 --- a/modules/geoedgeRtdProvider.md +++ b/modules/geoedgeRtdProvider.md @@ -5,7 +5,7 @@ Module Type: Rtd Provider Maintainer: guy.books@geoedge.com The Geoedge Realtime module lets publishers block bad ads such as automatic redirects, malware, offensive creatives and landing pages. -To use this module, you'll need to work with [Geoedge](https://www.geoedge.com/publishers-real-time-protection/) to get an account and cutomer key. +To use this module, you'll need to work with [Geoedge](https://www.geoedge.com/publishers-real-time-protection/) to get an account and customer key. ## Integration @@ -49,6 +49,7 @@ Parameters details: |params.key | String | Customer key |Required, contact Geoedge to get your key | |params.bidders | Object | Bidders to monitor |Optional, list of bidder to include / exclude from monitoring. Omitting this will monitor bids from all bidders. | |params.wap |Boolean |Wrap after preload |Optional, defaults to `false`. Set to `true` if you want to monitor only after the module has preloaded the monitoring client. | +|params.gpt |Boolean |Wrap all GPT ad slots |Optional, defaults to `false`. Set to `true` if you want to monitor all Google Publisher Tag ad slots, regaedless if the winning bid comes from Prebid or Google Ad Manager (Direct, Adx, Adesnse, Open Bidding, etc). | ## Example diff --git a/modules/geolocationRtdProvider.js b/modules/geolocationRtdProvider.js new file mode 100644 index 00000000000..b46a25e9246 --- /dev/null +++ b/modules/geolocationRtdProvider.js @@ -0,0 +1,63 @@ +import {submodule} from '../src/hook.js'; +import {isFn, logError, deepAccess, deepSetValue, logInfo, logWarn, timestamp} from '../src/utils.js'; +import { ACTIVITY_TRANSMIT_PRECISE_GEO } from '../src/activities/activities.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { isActivityAllowed } from '../src/activities/rules.js'; +import { activityParams } from '../src/activities/activityParams.js'; + +let permissionsAvailable = true; +let geolocation; +function getGeolocationData(requestBidsObject, onDone, providerConfig, userConsent) { + let done = false; + if (!permissionsAvailable) { + logWarn('permission for geolocation receiving was denied'); + return complete() + }; + if (!isActivityAllowed(ACTIVITY_TRANSMIT_PRECISE_GEO, activityParams(MODULE_TYPE_RTD, 'geolocation'))) { + logWarn('permission for geolocation receiving was denied by CMP'); + return complete() + }; + const requestPermission = deepAccess(providerConfig, 'params.requestPermission') === true; + navigator.permissions.query({ + name: 'geolocation', + }).then(permission => { + if (permission.state !== 'granted' && !requestPermission) return complete(); + navigator.geolocation.getCurrentPosition(geo => { + geolocation = geo; + complete(); + }); + }); + function complete() { + if (done) return; + done = true; + if (geolocation) { + deepSetValue(requestBidsObject, 'ortb2Fragments.global.device.geo', { + lat: geolocation.coords.latitude, + lon: geolocation.coords.longitude, + lastfix: Math.round((timestamp() - geolocation.timestamp) / 1000), + type: 1 + }); + logInfo('geolocation was successfully received ', requestBidsObject.ortb2Fragments.global.device.geo) + } + onDone(); + } +} +function init(moduleConfig) { + geolocation = void 0; + if (!isFn(navigator?.permissions?.query) || !isFn(navigator?.geolocation?.getCurrentPosition || !navigator?.permissions?.query)) { + logError('geolocation is not defined'); + permissionsAvailable = false; + } else { + permissionsAvailable = true; + } + return permissionsAvailable; +} +export const geolocationSubmodule = { + name: 'geolocation', + getBidRequestData: getGeolocationData, + init: init, +}; +function registerSubModule() { + submodule('realTimeData', geolocationSubmodule); +} +registerSubModule(); diff --git a/modules/getintentBidAdapter.js b/modules/getintentBidAdapter.js index 25322d81f9b..2b6ea1c2c2a 100644 --- a/modules/getintentBidAdapter.js +++ b/modules/getintentBidAdapter.js @@ -1,4 +1,4 @@ -import { getBidIdParameter, isFn, isInteger } from '../src/utils.js'; +import {getBidIdParameter, isFn, isInteger} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'getintent'; diff --git a/modules/gmosspBidAdapter.js b/modules/gmosspBidAdapter.js index 8c90d0cccfe..559f9f77aaf 100644 --- a/modules/gmosspBidAdapter.js +++ b/modules/gmosspBidAdapter.js @@ -1,17 +1,16 @@ import { createTrackPixelHtml, deepAccess, - deepSetValue, - getBidIdParameter, + deepSetValue, getBidIdParameter, getDNT, getWindowTop, isEmpty, - logError, - tryAppendQueryString + logError } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER} from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'gmossp'; const ENDPOINT = 'https://sp.gmossp-sp.jp/hb/prebid/query.ad'; diff --git a/modules/goldbachBidAdapter.js b/modules/goldbachBidAdapter.js index 4768931950c..8892df130df 100644 --- a/modules/goldbachBidAdapter.js +++ b/modules/goldbachBidAdapter.js @@ -1,15 +1,9 @@ import {Renderer} from '../src/Renderer.js'; import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, isArray, isArrayOfNums, @@ -29,9 +23,12 @@ import {auctionManager} from '../src/auctionManager.js'; import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import { APPNEXUS_CATEGORY_MAPPING } from '../libraries/categoryTranslationMapping/index.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {APPNEXUS_CATEGORY_MAPPING} from '../libraries/categoryTranslationMapping/index.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const BIDDER_CODE = 'goldbach'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -971,7 +968,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -997,7 +994,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration diff --git a/modules/goldfishAdsRtdProvider.js b/modules/goldfishAdsRtdProvider.js new file mode 100755 index 00000000000..c466ac5ec25 --- /dev/null +++ b/modules/goldfishAdsRtdProvider.js @@ -0,0 +1,194 @@ +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { deepAccess } from '../src/utils.js'; + +export const MODULE_NAME = 'goldfishAdsRtd'; +export const MODULE_TYPE = 'realTimeData'; +export const ENDPOINT_URL = 'https://prebid.goldfishads.com/iab-segments'; +export const DATA_STORAGE_KEY = 'goldfishads_data'; +export const DATA_STORAGE_TTL = 1800 * 1000// TTL in seconds + +export const ADAPTER_VERSION = '1.0'; + +export const storage = getStorageManager({ + gvlid: null, + moduleName: MODULE_NAME, + moduleType: MODULE_TYPE, +}); + +/** + * + * @param {{response: string[]} } response + * @returns + */ +export const manageCallbackResponse = (response) => { + try { + const foo = JSON.parse(response.response); + if (!Array.isArray(foo)) throw new Error('Invalid response'); + const enrichedResponse = { + ext: { + segtax: 4 + }, + segment: foo.map((segment) => { return { id: segment } }), + }; + const output = { + name: 'goldfishads.com', + ...enrichedResponse, + }; + return output; + } catch (e) { + throw e; + }; +}; + +/** + * @param {string} key + * @returns { Promise<{name: 'goldfishads.com', ext: { segtag: 4 }, segment: string[]}> } + */ + +const getTargetingDataFromApi = (key) => { + return new Promise((resolve, reject) => { + const requestOptions = { + customHeaders: { + 'Accept': 'application/json' + } + } + const callbacks = { + success(responseText, response) { + try { + const output = manageCallbackResponse(response); + resolve(output); + } catch (e) { + reject(e); + } + }, + error(error) { + reject(error); + } + }; + ajax(`${ENDPOINT_URL}?key=${key}`, callbacks, null, requestOptions) + }) +}; + +/** + * @returns {{ + * name: 'golfishads.com', + * ext: { segtax: 4}, + * segment: string[] + * } | null } + */ +export const getStorageData = () => { + const now = new Date(); + const data = storage.getDataFromLocalStorage(DATA_STORAGE_KEY); + if (data === null) return null; + try { + const foo = JSON.parse(data); + if (now.getTime() > foo.expiry) return null; + return foo.targeting; + } catch (e) { + return null; + } +}; + +/** + * @param { { key: string } } payload + * @returns {Promise<{ + * name: string, + * ext: { segtax: 4}, + * segment: string[] + * }> | null + * } + */ + +const getTargetingData = (payload) => new Promise((resolve) => { + const targeting = getStorageData(); + if (targeting === null) { + getTargetingDataFromApi(payload.key) + .then((response) => { + const now = new Date() + const data = { + targeting: response, + expiry: now.getTime() + DATA_STORAGE_TTL, + }; + storage.setDataInLocalStorage(DATA_STORAGE_KEY, JSON.stringify(data)); + resolve(response); + }) + .catch((e) => { + resolve(null); + }); + } else { + resolve(targeting); + } +}) + +/** + * + * @param {*} config + * @param {*} userConsent + * @returns {boolean} + */ + +const init = (config, userConsent) => { + if (!config.params || !config.params.key) return false; + // return { type: (typeof config.params.key === 'string') }; + if (!(typeof config.params.key === 'string')) return false; + return true; +}; + +/** + * + * @param {{ + * name: string, +* ext: { segtax: 4}, +* segment: {id: string}[] +* } | null } userData + * @param {*} reqBidsConfigObj + * @returns + */ +export const updateUserData = (userData, reqBidsConfigObj) => { + if (userData === null) return; + const bidders = ['appnexus', 'rubicon', 'nexx360']; + for (let i = 0; i < bidders.length; i++) { + const bidderCode = bidders[i]; + const originalConfig = deepAccess(reqBidsConfigObj, `ortb2Fragments.bidder[${bidderCode}].user.data`) || []; + const userConfig = [ + ...originalConfig, + userData, + ]; + reqBidsConfigObj.ortb2Fragments = reqBidsConfigObj.ortb2Fragments || {}; + reqBidsConfigObj.ortb2Fragments.bidder = reqBidsConfigObj.ortb2Fragments.bidder || {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode] = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode] || {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user = {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user.data = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user.data || userConfig; + } + return reqBidsConfigObj; +} + +/** + * + * @param {*} reqBidsConfigObj + * @param {*} callback + * @param {*} moduleConfig + * @param {*} userConsent + * @returns {void} + */ +const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, userConsent) => { + const payload = { + key: moduleConfig.params.key, + }; + getTargetingData(payload) + .then((userData) => { + updateUserData(userData, reqBidsConfigObj); + callback(); + }); +}; + +/** @type {RtdSubmodule} */ +export const goldfishAdsSubModule = { + name: MODULE_NAME, + init, + getBidRequestData, +}; + +submodule(MODULE_TYPE, goldfishAdsSubModule); diff --git a/modules/goldfishAdsRtdProvider.md b/modules/goldfishAdsRtdProvider.md new file mode 100755 index 00000000000..4625c9a7988 --- /dev/null +++ b/modules/goldfishAdsRtdProvider.md @@ -0,0 +1,48 @@ +# Goldfish Ads Real-time Data Submodule + +## Overview + + Module Name: Goldfish Ads Rtd Provider + Module Type: Rtd Provider + Maintainer: keith@goldfishads.com + +## Description + +This RTD module provides access to the Goldfish Ads Geograph, which leverages geographic and temporal data on a privcay-first platform. This module works without using cookies, PII, emails, or device IDs across all website traffic, including unauthenticated users, and adds audience data into bid requests to increase scale and yields. + +## Usage + +### Build +``` +gulp build --modules="rtdModule,goldfishAdsRtdProvider,appnexusBidAdapter,..." +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Goldfish Ads RTD module. + +### Configuration + +Use `setConfig` to instruct Prebid.js to initialize the Goldfish Ads RTD module, as specified below. + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [{ + name: 'goldfishAds', + waitForIt: true, + params: { + key: 'testkey' + } + }] + } +}) +``` + +### Parameters +| Name | Type | Description | Default | +|:-----------------|:----------------------------------------|:-----------------------------------------------------------------------------|:-----------------------| +| name | String | Real time data module name | Always 'goldfishAds' | +| waitForIt | Boolean | Set to true to maximize chance for bidder enrichment, used with auctionDelay | `false` | +| params.key | String | Your key id issued by Goldfish Ads | | diff --git a/modules/gppControl_usnat.js b/modules/gppControl_usnat.js new file mode 100644 index 00000000000..b38fc1a9d29 --- /dev/null +++ b/modules/gppControl_usnat.js @@ -0,0 +1,11 @@ +import {config} from '../src/config.js'; +import {setupRules} from '../libraries/mspa/activityControls.js'; + +let setupDone = false; + +config.getConfig('consentManagement', (cfg) => { + if (cfg?.consentManagement?.gpp != null && !setupDone) { + setupRules('usnat', [7]); + setupDone = true; + } +}) diff --git a/modules/gppControl_usstates.js b/modules/gppControl_usstates.js new file mode 100644 index 00000000000..bc2b434e085 --- /dev/null +++ b/modules/gppControl_usstates.js @@ -0,0 +1,176 @@ +import {config} from '../src/config.js'; +import {setupRules} from '../libraries/mspa/activityControls.js'; +import {deepSetValue, prefixLog} from '../src/utils.js'; + +const FIELDS = { + Version: 0, + Gpc: 0, + SharingNotice: 0, + SaleOptOutNotice: 0, + SharingOptOutNotice: 0, + TargetedAdvertisingOptOutNotice: 0, + SensitiveDataProcessingOptOutNotice: 0, + SensitiveDataLimitUseNotice: 0, + SaleOptOut: 0, + SharingOptOut: 0, + TargetedAdvertisingOptOut: 0, + SensitiveDataProcessing: 12, + KnownChildSensitiveDataConsents: 2, + PersonalDataConsents: 0, + MspaCoveredTransaction: 0, + MspaOptOutOptionMode: 0, + MspaServiceProviderMode: 0, +}; + +/** + * Generate a normalization function for converting US state strings to the usnat format. + * + * Scalar fields are copied over if they exist in the input (state) data, or set to null otherwise. + * List fields are also copied, but forced to the "correct" length (by truncating or padding with nulls); + * additionally, elements within them can be moved around using the `move` argument. + * + * @param {Array[String]} nullify? list of fields to force to null + * @param {{}} move? Map from list field name to an index remapping for elements within that field (using 1 as the first index). + * For example, {SensitiveDataProcessing: {1: 2, 2: [1, 3]}} means "rearrange SensitiveDataProcessing by moving + * the first element to the second position, and the second element to both the first and third position." + * @param {({}, {}) => void} fn? an optional function to run once all the processing described above is complete; + * it's passed two arguments, the original (state) data, and its normalized (usnat) version. + * @param fields + * @returns {function({}): {}} + */ +export function normalizer({nullify = [], move = {}, fn}, fields = FIELDS) { + move = Object.fromEntries(Object.entries(move).map(([k, map]) => [k, + Object.fromEntries(Object.entries(map) + .map(([k, v]) => [k, Array.isArray(v) ? v : [v]]) + .map(([k, v]) => [--k, v.map(el => --el)]) + )]) + ); + return function (cd) { + const norm = Object.fromEntries(Object.entries(fields) + .map(([field, len]) => { + let val = null; + if (len > 0) { + val = Array(len).fill(null); + if (Array.isArray(cd[field])) { + const remap = move[field] || {}; + const done = []; + cd[field].forEach((el, i) => { + const [dest, moved] = remap.hasOwnProperty(i) ? [remap[i], true] : [[i], false]; + dest.forEach(d => { + if (d < len && !done.includes(d)) { + val[d] = el; + moved && done.push(d); + } + }); + }); + } + } else if (cd[field] != null) { + val = Array.isArray(cd[field]) ? null : cd[field]; + } + return [field, val]; + })); + nullify.forEach(path => deepSetValue(norm, path, null)); + fn && fn(cd, norm); + return norm; + }; +} + +function scalarMinorsAreChildren(original, normalized) { + normalized.KnownChildSensitiveDataConsents = original.KnownChildSensitiveDataConsents === 0 ? [0, 0] : [1, 1]; +} + +export const NORMALIZATIONS = { + // normalization rules - convert state consent into usnat consent + // https://docs.prebid.org/features/mspa-usnat.html + 7: (consent) => consent, + 8: normalizer({ + move: { + SensitiveDataProcessing: { + 1: 9, + 2: 10, + 3: 8, + 4: [1, 2], + 5: 12, + 8: 3, + 9: 4, + } + }, + fn(original, normalized) { + if (original.KnownChildSensitiveDataConsents.some(el => el !== 0)) { + normalized.KnownChildSensitiveDataConsents = [1, 1]; + } + } + }), + 9: normalizer({fn: scalarMinorsAreChildren}), + 10: normalizer({fn: scalarMinorsAreChildren}), + 11: normalizer({ + move: { + SensitiveDataProcessing: { + 3: 4, + 4: 5, + 5: 3, + } + }, + fn: scalarMinorsAreChildren + }), + 12: normalizer({ + fn(original, normalized) { + const cc = original.KnownChildSensitiveDataConsents; + let repl; + if (!cc.some(el => el !== 0)) { + repl = [0, 0]; + } else if (cc[1] === 2 && cc[2] === 2) { + repl = [2, 1]; + } else { + repl = [1, 1]; + } + normalized.KnownChildSensitiveDataConsents = repl; + } + }) +}; + +export const DEFAULT_SID_MAPPING = { + 8: 'usca', + 9: 'usva', + 10: 'usco', + 11: 'usut', + 12: 'usct' +}; + +export const getSections = (() => { + const allSIDs = Object.keys(DEFAULT_SID_MAPPING).map(Number); + return function ({sections = {}, sids = allSIDs} = {}) { + return sids.map(sid => { + const logger = prefixLog(`Cannot set up MSPA controls for SID ${sid}:`); + const ov = sections[sid] || {}; + const normalizeAs = ov.normalizeAs || sid; + if (!NORMALIZATIONS.hasOwnProperty(normalizeAs)) { + logger.logError(`no normalization rules are known for SID ${normalizeAs}`) + return; + } + const api = ov.name || DEFAULT_SID_MAPPING[sid]; + if (typeof api !== 'string') { + logger.logError(`cannot determine GPP section name`) + return; + } + return [ + api, + [sid], + NORMALIZATIONS[normalizeAs] + ] + }).filter(el => el != null); + } +})(); + +const handles = []; + +config.getConfig('consentManagement', (cfg) => { + const gppConf = cfg.consentManagement?.gpp; + if (gppConf) { + while (handles.length) { + handles.pop()(); + } + getSections(gppConf?.mspa || {}) + .forEach(([api, sids, normalize]) => handles.push(setupRules(api, sids, normalize))); + } +}); diff --git a/modules/gptPreAuction.js b/modules/gptPreAuction.js index 71884235b38..bf5b4a55dbb 100644 --- a/modules/gptPreAuction.js +++ b/modules/gptPreAuction.js @@ -1,4 +1,11 @@ -import {deepAccess, isAdUnitCodeMatchingSlot, isGptPubadsDefined, logInfo, pick} from '../src/utils.js'; +import { + deepAccess, + isAdUnitCodeMatchingSlot, + isGptPubadsDefined, + logInfo, + pick, + deepSetValue +} from '../src/utils.js'; import {config} from '../src/config.js'; import {getHook} from '../src/hook.js'; import {find} from '../src/polyfill.js'; @@ -15,7 +22,8 @@ export const appendGptSlots = adUnits => { } const adUnitMap = adUnits.reduce((acc, adUnit) => { - acc[adUnit.code] = adUnit; + acc[adUnit.code] = acc[adUnit.code] || []; + acc[adUnit.code].push(adUnit); return acc; }, {}); @@ -25,15 +33,13 @@ export const appendGptSlots = adUnits => { : isAdUnitCodeMatchingSlot(slot)); if (matchingAdUnitCode) { - const adUnit = adUnitMap[matchingAdUnitCode]; - adUnit.ortb2Imp = adUnit.ortb2Imp || {}; - adUnit.ortb2Imp.ext = adUnit.ortb2Imp.ext || {}; - adUnit.ortb2Imp.ext.data = adUnit.ortb2Imp.ext.data || {}; - - const context = adUnit.ortb2Imp.ext.data; - context.adserver = context.adserver || {}; - context.adserver.name = 'gam'; - context.adserver.adslot = sanitizeSlotPath(slot.getAdUnitPath()); + const adserver = { + name: 'gam', + adslot: sanitizeSlotPath(slot.getAdUnitPath()) + }; + adUnitMap[matchingAdUnitCode].forEach((adUnit) => { + deepSetValue(adUnit, 'ortb2Imp.ext.data.adserver', Object.assign({}, adUnit.ortb2Imp?.ext?.data?.adserver, adserver)); + }); } }); }; diff --git a/modules/gravitoIdSystem.js b/modules/gravitoIdSystem.js index aa25ea7db2c..70031ebd06e 100644 --- a/modules/gravitoIdSystem.js +++ b/modules/gravitoIdSystem.js @@ -53,6 +53,12 @@ export const gravitoIdSystemSubmodule = { } return undefined; }, + eids: { + 'gravitompId': { + source: 'gravito.net', + atype: 1 + }, + } } submodule('userId', gravitoIdSystemSubmodule); diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index edc0c9c6c5c..2189172e16f 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -112,6 +112,7 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER ...(adUnit.mediaTypes.video !== undefined) && {video: adUnit.mediaTypes.video}, ...(adUnit.mediaTypes.native !== undefined) && {native: adUnit.mediaTypes.native} }, + ortb2Imp: adUnit.ortb2Imp || {}, bidders: [], }); }); diff --git a/modules/greenbidsRtdProvider.js b/modules/greenbidsRtdProvider.js index ef12326cf18..b3d79f05996 100644 --- a/modules/greenbidsRtdProvider.js +++ b/modules/greenbidsRtdProvider.js @@ -4,7 +4,7 @@ import { submodule } from '../src/hook.js'; const MODULE_NAME = 'greenbidsRtdProvider'; const MODULE_VERSION = '1.0.0'; -const ENDPOINT = 'https://europe-west1-greenbids-357713.cloudfunctions.net/partner-selection'; +const ENDPOINT = 'https://t.greenbids.ai'; const auctionInfo = {}; const rtdOptions = {}; diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index 9c63e871356..aa00a84273c 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -9,6 +9,7 @@ import { isNumber, isStr } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; @@ -17,6 +18,7 @@ import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'grid'; const ENDPOINT_URL = 'https://grid.bidswitch.net/hbjson'; +const USP_DELETE_DATA_HANDLER = 'https://media.grid.bidswitch.net/uspapi_delete' const SYNC_URL = 'https://x.bidswitch.net/sync?ssp=themediagrid'; const TIME_TO_LIVE = 360; @@ -26,10 +28,9 @@ const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const LOG_ERROR_MESS = { - noAuid: 'Bid from response has no auid parameter - ', + noAdid: 'Bid from response has no adid parameter - ', noAdm: 'Bid from response has no adm parameter - ', noBid: 'Array of bid objects is empty', - noPlacementCode: 'Can\'t find in requested bids the bid with auid - ', emptyUids: 'Uids should be not empty', emptySeatbid: 'Seatbid array from response has empty item', emptyResponse: 'Response is empty', @@ -90,7 +91,7 @@ export const spec = { let {bidderRequestId, gdprConsent, uspConsent, timeout, refererInfo, gppConsent} = bidderRequest || {}; const referer = refererInfo ? encodeURIComponent(refererInfo.page) : ''; - const tmax = timeout; + const tmax = parseInt(timeout) || null; const imp = []; const bidsMap = {}; const requests = []; @@ -132,20 +133,13 @@ export const spec = { }; if (ortb2Imp) { if (ortb2Imp.instl) { - impObj.instl = ortb2Imp.instl; + impObj.instl = parseInt(ortb2Imp.instl) || null; } if (ortb2Imp.ext) { + impObj.ext.gpid = ortb2Imp.ext.gpid?.toString() || ortb2Imp.ext.data?.pbadslot?.toString() || ortb2Imp.ext.data?.adserver?.adslot?.toString(); if (ortb2Imp.ext.data) { impObj.ext.data = ortb2Imp.ext.data; - if (impObj.ext.data.adserver && impObj.ext.data.adserver.adslot) { - impObj.ext.gpid = impObj.ext.data.adserver.adslot.toString(); - } else if (ortb2Imp.ext.data.pbadslot) { - impObj.ext.gpid = ortb2Imp.ext.data.pbadslot.toString(); - } - } - if (ortb2Imp.ext.gpid) { - impObj.ext.gpid = ortb2Imp.ext.gpid.toString(); } } } @@ -463,6 +457,23 @@ export const spec = { url: syncUrl + params }; } + }, + + ajaxCall: function(url, cb, data, options) { + return ajax(url, cb, data, options); + }, + + onDataDeletionRequest: function(data) { + const uids = []; + const aliases = [spec.code, ...spec.aliases.map((alias) => alias.code || alias)]; + data.forEach(({ bids }) => bids && bids.forEach(({ bidder, params }) => { + if (aliases.includes(bidder) && params && params.uid) { + uids.push(params.uid); + } + })); + if (uids.length) { + spec.ajaxCall(USP_DELETE_DATA_HANDLER, () => {}, JSON.stringify({ uids }), {contentType: 'application/json', method: 'POST'}); + } } }; @@ -474,7 +485,7 @@ export const spec = { */ function _getFloor (mediaTypes, bid) { const curMediaType = mediaTypes.video ? 'video' : 'banner'; - let floor = bid.params.bidFloor || bid.params.floorcpm || 0; + let floor = parseFloat(bid.params.bidFloor || bid.params.floorcpm || 0) || null; if (typeof bid.getFloor === 'function') { const floorInfo = bid.getFloor({ @@ -507,7 +518,7 @@ function _getBidFromResponse(respItem) { function _addBidResponse(serverBid, bidRequest, bidResponses, RendererConst, bidderCode) { if (!serverBid) return; let errorMessage; - if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid); + if (!serverBid.adid) errorMessage = LOG_ERROR_MESS.noAdid + JSON.stringify(serverBid); if (!errorMessage && !serverBid.adm && !serverBid.nurl) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); else { const bidObject = bidRequest.bidsMap ? bidRequest.bidsMap[serverBid.impid] : bidRequest.bidObject; @@ -518,7 +529,7 @@ function _addBidResponse(serverBid, bidRequest, bidResponses, RendererConst, bid cpm: serverBid.price, width: serverBid.w, height: serverBid.h, - creativeId: serverBid.auid, // bid.bidId + creativeId: serverBid.adid, currency: 'USD', netRevenue: true, ttl: TIME_TO_LIVE, @@ -584,8 +595,8 @@ function createVideoRequest(videoParams, mediaType, bidSizes) { if (!videoData.w || !videoData.h) return; - const minDur = mind || durationRangeSec[0] || videoData.minduration; - const maxDur = maxd || durationRangeSec[1] || videoData.maxduration; + const minDur = mind || durationRangeSec[0] || parseInt(videoData.minduration) || null; + const maxDur = maxd || durationRangeSec[1] || parseInt(videoData.maxduration) || null; if (minDur) { videoData.minduration = minDur; diff --git a/modules/growadvertisingBidAdapter.js b/modules/growadvertisingBidAdapter.js index b9b256dbaff..f6f7867f0fe 100644 --- a/modules/growadvertisingBidAdapter.js +++ b/modules/growadvertisingBidAdapter.js @@ -1,6 +1,6 @@ 'use strict'; -import { getBidIdParameter, deepAccess, _each, triggerPixel } from '../src/utils.js'; +import {deepAccess, _each, triggerPixel, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; diff --git a/modules/growthCodeAnalyticsAdapter.js b/modules/growthCodeAnalyticsAdapter.js index a2ab4ddbfac..5c7cc254f1d 100644 --- a/modules/growthCodeAnalyticsAdapter.js +++ b/modules/growthCodeAnalyticsAdapter.js @@ -13,7 +13,7 @@ import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_NAME = 'growthCodeAnalytics'; const DEFAULT_PID = 'INVALID_PID' -const ENDPOINT_URL = 'https://p2.gcprivacy.com/v1/pb/analytics' +const ENDPOINT_URL = 'https://analytics.gcprivacy.com/v3/pb/analytics' export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}); @@ -30,8 +30,8 @@ let bidRequestTimeout = 0; let analyticsType = 'endpoint'; let growthCodeAnalyticsAdapter = Object.assign(adapter({url: url, analyticsType}), { - track({eventType, eventData}) { - eventData = eventData ? JSON.parse(JSON.stringify(eventData)) : {}; + track({eventType, args}) { + let eventData = args ? JSON.parse(JSON.stringify(args)) : {}; let data = {}; if (!trackEvents.includes(eventType)) return; switch (eventType) { @@ -98,6 +98,11 @@ let growthCodeAnalyticsAdapter = Object.assign(adapter({url: url, analyticsType} break; } + case CONSTANTS.EVENTS.NO_BID: { + data = eventData + break; + } + default: return; } @@ -133,12 +138,15 @@ growthCodeAnalyticsAdapter.enableAnalytics = function(conf = {}) { function logToServer() { if (pid === DEFAULT_PID) return; - if (eventQueue.length > 1) { + if (eventQueue.length >= 1) { + // Get the correct GCID + let gcid = localStorage.getItem('gcid') + let data = { session: sessionId, pid: pid, + gcid: gcid, timestamp: Date.now(), - timezoneoffset: new Date().getTimezoneOffset(), url: getRefererInfo().page, referer: document.referrer, events: eventQueue @@ -162,7 +170,7 @@ function sendEvent(event) { eventQueue.push(event); logInfo(MODULE_NAME + 'Analytics Event: ' + event); - if (event.eventType === CONSTANTS.EVENTS.AUCTION_END) { + if ((event.eventType === CONSTANTS.EVENTS.AUCTION_END) || (event.eventType === CONSTANTS.EVENTS.BID_WON)) { logToServer(); } } diff --git a/modules/growthCodeAnalyticsAdapter.md b/modules/growthCodeAnalyticsAdapter.md index e45cb2e9c62..6625d492ee6 100644 --- a/modules/growthCodeAnalyticsAdapter.md +++ b/modules/growthCodeAnalyticsAdapter.md @@ -21,13 +21,7 @@ pbjs.enableAnalytics({ pid: '', trackEvents: [ 'auctionEnd', - 'bidAdjustment', - 'bidTimeout', - 'bidRequested', - 'bidResponse', - 'noBid', - 'bidWon', - 'bidderDone'] + 'bidWon'] } }); ``` diff --git a/modules/growthCodeIdSystem.js b/modules/growthCodeIdSystem.js index fae022f1a56..e50a4e73019 100644 --- a/modules/growthCodeIdSystem.js +++ b/modules/growthCodeIdSystem.js @@ -5,11 +5,12 @@ * @requires module:modules/userId */ -import {logError, logInfo, tryAppendQueryString} from '../src/utils.js'; +import {logError, logInfo, pick} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import { submodule } from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const MODULE_NAME = 'growthCodeId'; const GC_DATA_KEY = '_gc_data'; @@ -166,6 +167,25 @@ export const growthCodeIdSubmodule = { } }; return { callback: resp }; + }, + eids: { + 'growthCodeId': { + getValue: function(data) { + return data.gc_id + }, + source: 'growthcode.io', + atype: 1, + getUidExt: function(data) { + const extendedData = pick(data, [ + 'h1', + 'h2', + 'h3', + ]); + if (Object.keys(extendedData).length) { + return extendedData; + } + } + }, } }; diff --git a/modules/growthCodeRtdProvider.js b/modules/growthCodeRtdProvider.js index 370ace9a203..ef5c7906ad7 100644 --- a/modules/growthCodeRtdProvider.js +++ b/modules/growthCodeRtdProvider.js @@ -5,10 +5,11 @@ import { submodule } from '../src/hook.js' import { getStorageManager } from '../src/storageManager.js'; import { - logMessage, logError, tryAppendQueryString, mergeDeep + logMessage, logError, mergeDeep } from '../src/utils.js'; import * as ajax from '../src/ajax.js'; import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const MODULE_NAME = 'growthCodeRtd'; const LOG_PREFIX = 'GrowthCodeRtd: '; diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index d050af4ac8f..83fd726fde5 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -14,6 +14,7 @@ const JCSI = { t: 0, rq: 8, pbv: '$prebid.version$' } const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; const TIME_TO_LIVE = 60; const DELAY_REQUEST_TIME = 1800000; // setting to 30 mins +const pubProvidedIdSources = ['dac.co.jp', 'audigent.com', 'id5-sync.com', 'liveramp.com', 'intentiq.com', 'liveintent.com', 'crwdcntrl.net', 'quantcast.com', 'adserver.org', 'yahoo.com'] let invalidRequestIds = {}; let pageViewId = null; @@ -175,6 +176,7 @@ function _getVidParams(attributes) { linearity: li, startdelay: sd, placement: pt, + plcmt, protocols = [], playerSize = [] } = attributes; @@ -186,7 +188,7 @@ function _getVidParams(attributes) { pr = protocols.join(','); } - return { + const result = { mind, maxd, li, @@ -196,6 +198,11 @@ function _getVidParams(attributes) { viw, vih }; + // Add vplcmt property to the result object if plcmt is available + if (plcmt !== undefined && plcmt !== null) { + result.vplcmt = plcmt; + } + return result; } /** @@ -310,7 +317,23 @@ function buildRequests(validBidRequests, bidderRequest) { // ADTS-174 Removed unnecessary checks to fix failing test data.lt = lt; data.to = to; - + function jsoStringifynWithMaxLength(data, maxLength) { + let jsonString = JSON.stringify(data); + if (jsonString.length <= maxLength) { + return jsonString; + } else { + const truncatedData = data.slice(0, Math.floor(data.length * (maxLength / jsonString.length))); + jsonString = JSON.stringify(truncatedData); + return jsonString; + } + } + // Send filtered pubProvidedId's + if (userId && userId.pubProvidedId) { + let filteredData = userId.pubProvidedId.filter(item => pubProvidedIdSources.includes(item.source)); + let maxLength = 1800; // replace this with your desired maximum length + let truncatedJsonString = jsoStringifynWithMaxLength(filteredData, maxLength); + data.pubProvidedId = truncatedJsonString + } // ADJS-1286 Read id5 id linktype field if (userId && userId.id5id && userId.id5id.uid && userId.id5id.ext) { data.id5Id = userId.id5id.uid || null @@ -383,15 +406,11 @@ function buildRequests(validBidRequests, bidderRequest) { data.uspConsent = uspConsent; } if (gppConsent) { - data.gppConsent = { - gppString: bidderRequest.gppConsent.gppString, - gpp_sid: bidderRequest.gppConsent.applicableSections - } + data.gppString = bidderRequest.gppConsent.gppString ? bidderRequest.gppConsent.gppString : '' + data.gppSid = Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections.join(',') : '' } else if (!gppConsent && bidderRequest?.ortb2?.regs?.gpp) { - data.gppConsent = { - gppString: bidderRequest.ortb2.regs.gpp, - gpp_sid: bidderRequest.ortb2.regs.gpp_sid - }; + data.gppString = bidderRequest.ortb2.regs.gpp + data.gppSid = Array.isArray(bidderRequest.ortb2.regs.gpp_sid) ? bidderRequest.ortb2.regs.gpp_sid.join(',') : '' } if (coppa) { data.coppa = coppa; diff --git a/modules/hadronIdSystem.js b/modules/hadronIdSystem.js index 596bf9611e6..c60f0f812a4 100644 --- a/modules/hadronIdSystem.js +++ b/modules/hadronIdSystem.js @@ -110,6 +110,12 @@ export const hadronIdSubmodule = { ajax(url, callbacks, undefined, {method: 'GET'}); }; return {callback: resp}; + }, + eids: { + 'hadronId': { + source: 'audigent.com', + atype: 1 + }, } }; diff --git a/modules/hadronIdSystem.md b/modules/hadronIdSystem.md index 7521cca06ac..212030cbcd9 100644 --- a/modules/hadronIdSystem.md +++ b/modules/hadronIdSystem.md @@ -35,5 +35,4 @@ The below parameters apply only to the HadronID User ID Module integration. | value | Optional | Object | Used only if the page has a separate mechanism for storing the Hadron ID. The value is an object containing the values to be sent to the adapters. In this scenario, no URL is called and nothing is added to local storage | `{"hadronId": "eb33b0cb-8d35-4722-b9c0-1a31d4064888"}` | | params | Optional | Object | Used to store params for the id system | | params.partnerId | Required | Number | This is the Audigent Partner ID obtained from Audigent. | `1234` | -| params.url | Optional | String | Set an alternate GET url for HadronId with this parameter | -| params.urlArg | Optional | Object | Optional url parameter for params.url | + | diff --git a/modules/holidBidAdapter.js b/modules/holidBidAdapter.js index f41829bd123..fbcbb9492c7 100644 --- a/modules/holidBidAdapter.js +++ b/modules/holidBidAdapter.js @@ -1,4 +1,10 @@ -import {deepAccess, getBidIdParameter, isStr, logMessage, triggerPixel, } from '../src/utils.js'; +import { + deepAccess, + deepSetValue, getBidIdParameter, + isStr, + logMessage, + triggerPixel, +} from '../src/utils.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import {BANNER} from '../src/mediaTypes.js'; @@ -10,6 +16,7 @@ const GVLID = 1177 const ENDPOINT = 'https://helloworld.holid.io/openrtb2/auction' const COOKIE_SYNC_ENDPOINT = 'https://null.holid.io/sync.html' const TIME_TO_LIVE = 300 +const TMAX = 500 let wurlMap = {} events.on(CONSTANTS.EVENTS.BID_WON, bidWonHandler) @@ -23,12 +30,19 @@ export const spec = { return !!bid.params.adUnitID }, - buildRequests: function (validBidRequests, _bidderRequest) { + buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map((bid) => { const requestData = { ...bid.ortb2, - id: _bidderRequest.bidderRequestId, + source: {schain: bid.schain}, + id: bidderRequest.bidderRequestId, imp: [getImp(bid)], + tmax: TMAX, + ...buildStoredRequest(bid) + } + + if (bid.userIdAsEids) { + deepSetValue(requestData, 'user.ext.eids', bid.userIdAsEids) } return { @@ -50,8 +64,6 @@ export const spec = { serverResponse.body.seatbid.map((response) => { response.bid.map((bid) => { const requestId = bidRequest.bidId - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - const auctionId = bidRequest.auctionId const wurl = deepAccess(bid, 'ext.prebid.events.win') const bidResponse = { requestId, @@ -65,7 +77,7 @@ export const spec = { ttl: TIME_TO_LIVE, } - addWurl({ auctionId, requestId, wurl }) + addWurl(requestId, wurl) bidResponses.push(bidResponse) }) @@ -75,11 +87,15 @@ export const spec = { }, getUserSyncs(optionsType, serverResponse, gdprConsent, uspConsent) { + const syncs = [{ + type: 'image', + url: 'https://track.adform.net/Serving/TrackPoint/?pm=2992097&lid=132720821' + }] + if (!serverResponse || serverResponse.length === 0) { - return [] + return syncs } - const syncs = [] const bidders = getBidders(serverResponse) if (optionsType.iframeEnabled && bidders) { @@ -100,24 +116,14 @@ export const spec = { type: 'iframe', url: COOKIE_SYNC_ENDPOINT + strQueryParams + '&type=iframe', }) - - return syncs } - return [] + return syncs }, } function getImp(bid) { - const imp = { - ext: { - prebid: { - storedrequest: { - id: getBidIdParameter('adUnitID', bid.params), - }, - }, - }, - } + const imp = buildStoredRequest(bid) const sizes = bid.sizes && !Array.isArray(bid.sizes[0]) ? [bid.sizes] : bid.sizes @@ -132,6 +138,18 @@ function getImp(bid) { return imp } +function buildStoredRequest(bid) { + return { + ext: { + prebid: { + storedrequest: { + id: getBidIdParameter('adUnitID', bid.params), + }, + }, + }, + } +} + function getBidders(serverResponse) { const bidders = serverResponse .map((res) => Object.keys(res.body.ext.responsetimemillis || [])) @@ -142,28 +160,28 @@ function getBidders(serverResponse) { } } -function addWurl(auctionId, adId, wurl) { - if ([auctionId, adId].every(isStr)) { - wurlMap[`${auctionId}${adId}`] = wurl +function addWurl(requestId, wurl) { + if (isStr(requestId)) { + wurlMap[requestId] = wurl } } -function removeWurl(auctionId, adId) { - delete wurlMap[`${auctionId}${adId}`] +function removeWurl(requestId) { + delete wurlMap[requestId] } -function getWurl(auctionId, adId) { - if ([auctionId, adId].every(isStr)) { - return wurlMap[`${auctionId}${adId}`] +function getWurl(requestId) { + if (isStr(requestId)) { + return wurlMap[requestId] } } function bidWonHandler(bid) { - const wurl = getWurl(bid.auctionId, bid.adId) + const wurl = getWurl(bid.requestId) if (wurl) { logMessage(`Invoking image pixel for wurl on BID_WIN: "${wurl}"`) triggerPixel(wurl) - removeWurl(bid.auctionId, bid.adId) + removeWurl(bid.requestId) } } diff --git a/modules/iasRtdProvider.js b/modules/iasRtdProvider.js index 994be7c0804..b9de7ef4e46 100644 --- a/modules/iasRtdProvider.js +++ b/modules/iasRtdProvider.js @@ -1,7 +1,9 @@ -import { submodule } from '../src/hook.js'; +import {submodule} from '../src/hook.js'; import * as utils from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {ajax} from '../src/ajax.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -76,7 +78,7 @@ function getAdUnitPath(adSlot, bidRequest, adUnitPath) { if (!utils.isEmpty(adSlot)) { p = adSlot.gptSlot; } else { - if (!utils.isEmpty(adUnitPath) && utils.hasOwn(adUnitPath, bidRequest.code)) { + if (!utils.isEmpty(adUnitPath) && adUnitPath.hasOwnProperty(bidRequest.code)) { if (utils.isStr(adUnitPath[bidRequest.code]) && !utils.isEmpty(adUnitPath[bidRequest.code])) { p = adUnitPath[bidRequest.code]; } @@ -86,13 +88,13 @@ function getAdUnitPath(adSlot, bidRequest, adUnitPath) { } function stringifySlot(bidRequest, adUnitPath) { - const sizes = utils.getAdUnitSizes(bidRequest); + const sizes = getAdUnitSizes(bidRequest); const id = bidRequest.code; const ss = stringifySlotSizes(sizes); - const adSlot = utils.getGptSlotInfoForAdUnitCode(bidRequest.code); + const adSlot = getGptSlotInfoForAdUnitCode(bidRequest.code); const p = getAdUnitPath(adSlot, bidRequest, adUnitPath); const slot = { id, ss, p }; - const keyValues = utils.getKeys(slot).map(function (key) { + const keyValues = Object.keys(slot).map(function (key) { return [key, slot[key]].join(':'); }); return '{' + keyValues.join(',') + '}'; diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index bb8a21264d0..e12aea5f8d1 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -108,7 +108,7 @@ export const id5IdSubmodule = { * @returns {IdResponse|undefined} */ getId(submoduleConfig, consentData, cacheIdObj) { - if (!hasRequiredConfig(submoduleConfig)) { + if (!validateConfig(submoduleConfig)) { return undefined; } @@ -142,19 +142,31 @@ export const id5IdSubmodule = { * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ extendId(config, consentData, cacheIdObj) { - hasRequiredConfig(config); - if (!hasWriteConsentToLocalStorage(consentData)) { logInfo(LOG_PREFIX + 'No consent given for ID5 local storage writing, skipping nb increment.') return cacheIdObj; } - const partnerId = (config && config.params && config.params.partner) || 0; + const partnerId = validateConfig(config) ? config.params.partner : 0; incrementNb(partnerId); logInfo(LOG_PREFIX + 'using cached ID', cacheIdObj); return cacheIdObj; - } + }, + eids: { + 'id5id': { + getValue: function(data) { + return data.uid + }, + source: 'id5-sync.com', + atype: 1, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + }, }; class IdFetchFlow { @@ -293,9 +305,23 @@ class IdFetchFlow { } } -function hasRequiredConfig(config) { - if (!config || !config.params || !config.params.partner || typeof config.params.partner !== 'number') { - logError(LOG_PREFIX + 'partner required to be defined as a number'); +function validateConfig(config) { + if (!config || !config.params || !config.params.partner) { + logError(LOG_PREFIX + 'partner required to be defined'); + return false; + } + + const partner = config.params.partner + if (typeof partner === 'string' || partner instanceof String) { + let parsedPartnerId = parseInt(partner); + if (isNaN(parsedPartnerId) || parsedPartnerId < 0) { + logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer'); + return false; + } else { + config.params.partner = parsedPartnerId; + } + } else if (typeof partner !== 'number') { + logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer'); return false; } diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md index cf90290b1d8..927fa10f87b 100644 --- a/modules/id5IdSystem.md +++ b/modules/id5IdSystem.md @@ -1,6 +1,6 @@ -# ID5 Universal ID +# ID5 ID -The ID5 ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 ID and detailed integration docs, please visit [our documentation](https://support.id5.io/portal/en/kb/articles/prebid-js-user-id-module). +The ID5 ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 ID and detailed integration docs, please visit [our documentation](https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/prebid-user-id-module/id5-prebid-user-id-module). ## ID5 ID Registration @@ -29,7 +29,7 @@ pbjs.setConfig({ abTesting: { // optional enabled: true, // false by default controlGroupPct: 0.1 // valid values are 0.0 - 1.0 (inclusive) - }, + }, disableExtensions: false // optional }, storage: { @@ -49,7 +49,7 @@ pbjs.setConfig({ | name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | | params | Required | Object | Details for the ID5 ID. | | | params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | -| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://support.id5.io/portal/en/kb/articles/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | +| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | | params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | | params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default | | params.abTesting.enabled | Optional | Boolean | Set this to `true` to turn on this feature | `true` or `false` | @@ -68,3 +68,6 @@ pbjs.setConfig({ Publishers may want to test the value of the ID5 ID with their downstream partners. While there are various ways to do this, A/B testing is a standard approach. Instead of publishers manually enabling or disabling the ID5 User ID Module based on their control group settings (which leads to fewer calls to ID5, reducing our ability to recognize the user), we have baked this in to our module directly. To turn on A/B Testing, simply edit the configuration (see above table) to enable it and set what percentage of users you would like to set for the control group. The control group is the set of user where an ID5 ID will not be exposed in to bid adapters or in the various user id functions available on the `pbjs` global. An additional value of `ext.abTestingControlGroup` will be set to `true` or `false` that can be used to inform reporting systems that the user was in the control group or not. It's important to note that the control group is user based, and not request based. In other words, from one page view to another, a user will always be in or out of the control group. + +### A Note on Using Multiple Wrappers +If you or your monetization partners are deploying multiple Prebid wrappers on your websites, you should make sure you add the ID5 ID User ID module to *every* wrapper. Only the bidders configured in the Prebid wrapper where the ID5 ID User ID module is installed and configured will be able to pick up the ID5 ID. Bidders from other Prebid instances will not be able to pick up the ID5 ID. diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index ab10288f38f..ba794df1a9c 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -15,6 +15,8 @@ const MODULE_NAME = 'identityLink'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); +const liverampEnvelopeName = '_lr_env'; + /** @type {Submodule} */ export const identityLinkSubmodule = { /** @@ -74,11 +76,24 @@ export const identityLinkSubmodule = { } }); } else { - getEnvelope(url, callback, configParams); + // try to get envelope directly from storage if ats lib is not present on a page + let envelope = getEnvelopeFromStorage(); + if (envelope) { + utils.logInfo('identityLink: LiveRamp envelope successfully retrieved from storage!'); + callback(JSON.parse(envelope).envelope); + } else { + getEnvelope(url, callback, configParams); + } } }; return { callback: resp }; + }, + eids: { + 'idl_env': { + source: 'liveramp.com', + atype: 3 + }, } }; // return envelope from third party endpoint @@ -121,4 +136,9 @@ function setEnvelopeSource(src) { storage.setCookie('_lr_env_src_ats', src, now.toUTCString()); } +export function getEnvelopeFromStorage() { + let rawEnvelope = storage.getCookie(liverampEnvelopeName) || storage.getDataFromLocalStorage(liverampEnvelopeName); + return rawEnvelope ? window.atob(rawEnvelope) : undefined; +} + submodule('userId', identityLinkSubmodule); diff --git a/modules/idxIdSystem.js b/modules/idxIdSystem.js index 3c00bbde34c..bf807f199a6 100644 --- a/modules/idxIdSystem.js +++ b/modules/idxIdSystem.js @@ -57,6 +57,12 @@ export const idxIdSubmodule = { } } return undefined; + }, + eids: { + 'idx': { + source: 'idx.lat', + atype: 1 + }, } }; submodule('userId', idxIdSubmodule); diff --git a/modules/illuminBidAdapter.js b/modules/illuminBidAdapter.js new file mode 100644 index 00000000000..36ff7bea0a3 --- /dev/null +++ b/modules/illuminBidAdapter.js @@ -0,0 +1,336 @@ +import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {config} from '../src/config.js'; + +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'illumin'; +const BIDDER_VERSION = '1.0.0'; +const CURRENCY = 'USD'; +const TTL_SECONDS = 60 * 5; +const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +function getTopWindowQueryParams() { + try { + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.illumin.com`; +} + +export function extractCID(params) { + return params.cId; +} + +export function extractPID(params) { + return params.pId; +} + +export function extractSubDomain(params) { + return params.subDomain; +} + +function isBidRequestValid(bid) { + const params = bid.params || {}; + return !!(extractCID(params) && extractPID(params)); +} + +function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const { + params, + bidId, + userId, + adUnitCode, + schain, + mediaTypes, + ortb2Imp, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + let {bidFloor, ext} = params; + const hashUrl = hashCode(topWindowUrl); + const uniqueDealId = getUniqueDealId(hashUrl); + const cId = extractCID(params); + const pId = extractPID(params); + const subDomain = extractSubDomain(params); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); + + if (isFn(bid.getFloor)) { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (floorInfo.currency === 'USD') { + bidFloor = floorInfo.floor; + } + } + + let data = { + url: encodeURIComponent(topWindowUrl), + uqs: getTopWindowQueryParams(), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + referrer: bidderRequest.refererInfo.ref, + adUnitCode: adUnitCode, + publisherId: pId, + sizes: sizes, + uniqueDealId: uniqueDealId, + bidderVersion: BIDDER_VERSION, + prebidVersion: '$prebid.version$', + res: `${screen.width}x${screen.height}`, + schain: schain, + mediaTypes: mediaTypes, + gpid: gpid, + transactionId: ortb2Imp?.ext?.tid, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout + }; + + appendUserIdsToRequestPayload(data, userId); + + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + if (bidderRequest.uspConsent) { + data.usPrivacy = bidderRequest.uspConsent; + } + + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + const dto = { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: data + }; + + _each(ext, (value, key) => { + dto.data['ext.' + key] = value; + }); + + return dto; +} + +function appendUserIdsToRequestPayload(payloadRef, userIds) { + let key; + _each(userIds, (userId, idSystemProviderName) => { + key = `uid.${idSystemProviderName}`; + + switch (idSystemProviderName) { + case 'digitrustid': + payloadRef[key] = deepAccess(userId, 'data.id'); + break; + case 'lipb': + payloadRef[key] = userId.lipbid; + break; + case 'parrableId': + payloadRef[key] = userId.eid; + break; + case 'id5id': + payloadRef[key] = userId.uid; + break; + default: + payloadRef[key] = userId; + } + }); +} + +function buildRequests(validBidRequests, bidderRequest) { + const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = config.getConfig('bidderTimeout'); + const requests = []; + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + return requests; +} + +function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + + try { + results.forEach(result => { + const { + creativeId, + ad, + price, + exp, + width, + height, + currency, + metaData, + advertiserDomains, + mediaType = BANNER + } = result; + if (!ad || !price) { + return; + } + + const response = { + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + }; + + if (metaData) { + Object.assign(response, { + meta: metaData + }) + } else { + Object.assign(response, { + meta: { + advertiserDomains: advertiserDomains || [] + } + }) + } + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); + }); + return output; + } catch (e) { + return []; + } +} + +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { + let syncs = []; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; + + const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); + const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + if (iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `https://sync.illumin.com/api/sync/iframe/${params}` + }); + } + if (pixelEnabled) { + syncs.push({ + type: 'image', + url: `https://sync.illumin.com/api/sync/image/${params}` + }); + } + return syncs; +} + +export function hashCode(s, prefix = '_') { + const l = s.length; + let h = 0 + let i = 0; + if (l > 0) { + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } + } + return prefix + h; +} + +export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { + const storageKey = `u_${key}`; + const now = Date.now(); + const data = getStorageItem(storageKey); + let uniqueId; + + if (!data || !data.value || now - data.created > expiry) { + uniqueId = `${key}_${now.toString()}`; + setStorageItem(storageKey, uniqueId); + } else { + uniqueId = data.value; + } + + return uniqueId; +} + +export function getStorageItem(key) { + try { + return tryParseJSON(storage.getDataFromLocalStorage(key)); + } catch (e) { + } + + return null; +} + +export function setStorageItem(key, value, timestamp) { + try { + const created = timestamp || Date.now(); + const data = JSON.stringify({value, created}); + storage.setDataInLocalStorage(key, data); + } catch (e) { + } +} + +export function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/illuminBidAdapter.md b/modules/illuminBidAdapter.md new file mode 100644 index 00000000000..8ca656230e4 --- /dev/null +++ b/modules/illuminBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name:** Illumin Bid Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** integrations@illumin.com + +# Description + +Module that connects to Illumin's demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'illumin', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; +``` diff --git a/modules/imdsBidAdapter.js b/modules/imdsBidAdapter.js index 545a0bd1ac3..4cad1d614c5 100644 --- a/modules/imdsBidAdapter.js +++ b/modules/imdsBidAdapter.js @@ -1,14 +1,16 @@ 'use strict'; -import {deepAccess, deepSetValue, getAdUnitSizes, isFn, isPlainObject, logWarn} from '../src/utils.js'; +import {deepAccess, deepSetValue, isFn, isPlainObject, logWarn, mergeDeep} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {includes} from '../src/polyfill.js'; import {config} from '../src/config.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BID_SCHEME = 'https://'; const BID_DOMAIN = 'technoratimedia.com'; -const USER_SYNC_HOST = 'https://ad-cdn.technoratimedia.com'; +const USER_SYNC_IFRAME_URL = 'https://ad-cdn.technoratimedia.com/html/usersync.html'; +const USER_SYNC_PIXEL_URL = 'https://sync.technoratimedia.com/services'; const VIDEO_PARAMS = [ 'minduration', 'maxduration', 'startdelay', 'placement', 'linearity', 'mimes', 'protocols', 'api' ]; const BLOCKED_AD_SIZES = [ '1x1', @@ -38,11 +40,11 @@ export const spec = { return; } const refererInfo = bidderRequest.refererInfo; - const openRtbBidRequest = { + // start with some defaults, overridden by anything set in ortb2, if provided. + const openRtbBidRequest = mergeDeep({ id: bidderRequest.bidderRequestId, site: { - // TODO: does the fallback make sense here? - domain: refererInfo.domain || location.hostname, + domain: refererInfo.domain, page: refererInfo.page, ref: refererInfo.ref }, @@ -50,7 +52,7 @@ export const spec = { ua: navigator.userAgent }, imp: [] - }; + }, bidderRequest.ortb2 || {}); const tmax = bidderRequest.timeout; if (tmax) { @@ -101,9 +103,17 @@ export const spec = { } }); - // CCPA - if (bidderRequest.uspConsent) { - deepSetValue(openRtbBidRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); + // Move us_privacy from regs.ext to regs if there isn't already a us_privacy in regs + if (openRtbBidRequest.regs?.ext?.us_privacy && !openRtbBidRequest.regs?.us_privacy) { + deepSetValue(openRtbBidRequest, 'regs.us_privacy', openRtbBidRequest.regs.ext.us_privacy); + } + + // Remove regs.ext.us_privacy + if (openRtbBidRequest.regs?.ext?.us_privacy) { + delete openRtbBidRequest.regs.ext.us_privacy; + if (Object.keys(openRtbBidRequest.regs.ext).length < 1) { + delete openRtbBidRequest.regs.ext; + } } // User ID @@ -117,7 +127,7 @@ export const spec = { if (openRtbBidRequest.imp.length && seatId) { return { method: 'POST', - url: `${BID_SCHEME}${seatId}.${BID_DOMAIN}/openrtb/bids/${seatId}?src=$$REPO_AND_VERSION$$`, + url: `${BID_SCHEME}${seatId}.${BID_DOMAIN}/openrtb/bids/${seatId}?src=pbjs%2F$prebid.version$`, data: openRtbBidRequest, options: { contentType: 'application/json', @@ -214,7 +224,6 @@ export const spec = { }; if (!serverResponse.body || typeof serverResponse.body != 'object') { - logWarn('IMDS: server returned empty/non-json response: ' + JSON.stringify(serverResponse.body)); return; } const {id, seatbid: seatbids} = serverResponse.body; @@ -294,16 +303,31 @@ export const spec = { } return bids; }, - getUserSyncs: function (syncOptions, serverResponses) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { const syncs = []; + const queryParams = ['src=pbjs%2F$prebid.version$']; + if (gdprConsent) { + queryParams.push(`gdpr=${Number(gdprConsent.gdprApplies && 1)}&consent=${encodeURIComponent(gdprConsent.consentString || '')}`); + } + if (uspConsent) { + queryParams.push('us_privacy=' + encodeURIComponent(uspConsent)); + } + if (gppConsent) { + queryParams.push('gpp=' + encodeURIComponent(gppConsent.gppString || '') + '&gppsid=' + encodeURIComponent((gppConsent.applicableSections || []).join(','))); + } + if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: `${USER_SYNC_HOST}/html/usersync.html?src=$$REPO_AND_VERSION$$` + url: `${USER_SYNC_IFRAME_URL}?${queryParams.join('&')}` + }); + } else if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: `${USER_SYNC_PIXEL_URL}?srv=cs&${queryParams.join('&')}` }); - } else { - logWarn('IMDS: Please enable iframe based user sync.'); } + return syncs; } }; diff --git a/modules/imdsBidAdapter.md b/modules/imdsBidAdapter.md index 15fb407e7ef..2a50868d726 100644 --- a/modules/imdsBidAdapter.md +++ b/modules/imdsBidAdapter.md @@ -11,11 +11,11 @@ Maintainer: eng-demand@imds.tv The iMedia Digital Services adapter requires setup and approval from iMedia Digital Services. Please reach out to your account manager for more information. -### DFP Video Creative -To use video, setup a `VAST redirect` creative within Google AdManager (DFP) with the following VAST tag URL: +### Google Ad Manager Video Creative +To use video, setup a `VAST redirect` creative within Google Ad Manager with the following VAST tag URL: -``` -https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_cache_id_synacorm%%&AUCTION_PRICE=%%PATTERN:hb_pb_synacormedia%% +```text +https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_uuid_imds%%&AUCTION_PRICE=%%PATTERN:hb_pb_imds%% ``` # Test Parameters diff --git a/modules/impactifyBidAdapter.js b/modules/impactifyBidAdapter.js index f2bf9aaddcb..f446050265a 100644 --- a/modules/impactifyBidAdapter.js +++ b/modules/impactifyBidAdapter.js @@ -1,7 +1,10 @@ -import {deepAccess, deepSetValue, generateUUID} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {config} from '../src/config.js'; -import {ajax} from '../src/ajax.js'; +'use strict'; + +import { deepAccess, deepSetValue, generateUUID } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { ajax } from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'impactify'; const BIDDER_ALIAS = ['imp']; @@ -10,35 +13,101 @@ const DEFAULT_VIDEO_WIDTH = 640; const DEFAULT_VIDEO_HEIGHT = 360; const ORIGIN = 'https://sonic.impactify.media'; const LOGGER_URI = 'https://logger.impactify.media'; -const AUCTIONURI = '/bidder'; -const COOKIESYNCURI = '/static/cookie_sync.html'; -const GVLID = 606; -const GETCONFIG = config.getConfig; - -const getDeviceType = () => { - // OpenRTB Device type - if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { - return 5; - } - if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { - return 4; - } - return 2; -}; +const AUCTION_URI = '/bidder'; +const COOKIE_SYNC_URI = '/static/cookie_sync.html'; +const GVL_ID = 606; +const GET_CONFIG = config.getConfig; +export const STORAGE = getStorageManager({gvlid: GVL_ID, bidderCode: BIDDER_CODE}); +export const STORAGE_KEY = '_im_str' + +/** + * Helpers object + * @type {{getExtParamsFromBid(*): {impactify: {appId}}, createOrtbImpVideoObj(*): {context: string, playerSize: [number,number], id: string, mimes: [string]}, getDeviceType(): (number), createOrtbImpBannerObj(*, *): {format: [], id: string}}} + */ +const helpers = { + getExtParamsFromBid(bid) { + let ext = { + impactify: { + appId: bid.params.appId + }, + }; -const getFloor = (bid) => { - const floorInfo = bid.getFloor({ - currency: DEFAULT_CURRENCY, - mediaType: '*', - size: '*' - }); - if (typeof floorInfo === 'object' && floorInfo.currency === DEFAULT_CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { - return parseFloat(floorInfo.floor); + if (typeof bid.params.format == 'string') { + ext.impactify.format = bid.params.format; + } + + if (typeof bid.params.style == 'string') { + ext.impactify.style = bid.params.style; + } + + if (typeof bid.params.container == 'string') { + ext.impactify.container = bid.params.container; + } + + if (typeof bid.params.size == 'string') { + ext.impactify.size = bid.params.size; + } + + return ext; + }, + + getDeviceType() { + // OpenRTB Device type + if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { + return 5; + } + if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { + return 4; + } + return 2; + }, + + createOrtbImpBannerObj(bid, size) { + let sizes = size.split('x'); + + return { + id: 'banner-' + bid.bidId, + format: [{ + w: parseInt(sizes[0]), + h: parseInt(sizes[1]) + }] + } + }, + + createOrtbImpVideoObj(bid) { + return { + id: 'video-' + bid.bidId, + playerSize: [DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT], + context: 'outstream', + mimes: ['video/mp4'], + } + }, + + getFloor(bid) { + const floorInfo = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*' + }); + if (typeof floorInfo === 'object' && floorInfo.currency === DEFAULT_CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + return parseFloat(floorInfo.floor); + } + return null; + }, + + getImStrFromLocalStorage() { + return STORAGE.localStorageIsEnabled(false) ? STORAGE.getDataFromLocalStorage(STORAGE_KEY, false) : ''; } - return null; + } -const createOpenRtbRequest = (validBidRequests, bidderRequest) => { +/** + * Create an OpenRTB formated object from prebid payload + * @param validBidRequests + * @param bidderRequest + * @returns {{cur: string[], validBidRequests, id, source: {tid}, imp: *[]}} + */ +function createOpenRtbRequest(validBidRequests, bidderRequest) { // Create request and set imp bids inside let request = { id: bidderRequest.bidderRequestId, @@ -52,16 +121,17 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const checkPrebid = urlParams.get('_checkPrebid'); - // Force impactify debugging parameter + + // Force impactify debugging parameter if present if (checkPrebid != null) { request.test = Number(checkPrebid); } - // Set Schain in request + // Set SChain in request let schain = deepAccess(validBidRequests, '0.schain'); if (schain) request.source.ext = { schain: schain }; - // Set eids + // Set Eids let eids = deepAccess(validBidRequests, '0.userIdAsEids'); if (eids && eids.length) { deepSetValue(request, 'user.ext.eids', eids); @@ -73,7 +143,7 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { request.device = { w: window.innerWidth, h: window.innerHeight, - devicetype: getDeviceType(), + devicetype: helpers.getDeviceType(), ua: navigator.userAgent, js: 1, dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, @@ -91,9 +161,10 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { if (bidderRequest.uspConsent) { deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); + this.syncStore.uspConsent = bidderRequest.uspConsent; } - if (GETCONFIG('coppa') == true) deepSetValue(request, 'regs.coppa', 1); + if (GET_CONFIG('coppa') == true) deepSetValue(request, 'regs.coppa', 1); if (bidderRequest.uspConsent) { deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); @@ -104,42 +175,50 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { // Create imps with bids validBidRequests.forEach((bid) => { + let bannerObj = deepAccess(bid.mediaTypes, `banner`); + let videoObj = deepAccess(bid.mediaTypes, `video`); + let imp = { id: bid.bidId, bidfloor: bid.params.bidfloor ? bid.params.bidfloor : 0, - ext: { - impactify: { - appId: bid.params.appId, - format: bid.params.format, - style: bid.params.style - }, - }, - video: { - playerSize: [DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT], - context: 'outstream', - mimes: ['video/mp4'], - }, + ext: helpers.getExtParamsFromBid(bid) }; - if (bid.params.container) { - imp.ext.impactify.container = bid.params.container; + + if (videoObj) { + imp.video = { + ...helpers.createOrtbImpVideoObj(bid) + } } + + if (bannerObj && typeof imp.ext.impactify.size == 'string') { + imp.banner = { + ...helpers.createOrtbImpBannerObj(bid, imp.ext.impactify.size) + } + } + if (typeof bid.getFloor === 'function') { - const floor = getFloor(bid); + const floor = helpers.getFloor(bid); if (floor) { imp.bidfloor = floor; } } + request.imp.push(imp); }); return request; -}; +} +/** + * Export BidderSpec type object and register it to Prebid + * @type {{supportedMediaTypes: string[], interpretResponse: ((function(ServerResponse, *): Bid[])|*), code: string, aliases: string[], getUserSyncs: ((function(SyncOptions, ServerResponse[], *, *): UserSync[])|*), buildRequests: (function(*, *): {method: string, data: string, url}), onTimeout: (function(*): boolean), gvlid: number, isBidRequestValid: ((function(BidRequest): (boolean))|*), onBidWon: (function(*): boolean)}} + */ export const spec = { code: BIDDER_CODE, - gvlid: GVLID, + gvlid: GVL_ID, supportedMediaTypes: ['video', 'banner'], aliases: BIDDER_ALIAS, + storageAllowed: true, /** * Determines whether or not the given bid request is valid. @@ -148,13 +227,21 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - if (!bid.params.appId || typeof bid.params.appId != 'string' || !bid.params.format || typeof bid.params.format != 'string' || !bid.params.style || typeof bid.params.style != 'string') { + let isBanner = deepAccess(bid.mediaTypes, `banner`); + + if (typeof bid.params.appId != 'string' || !bid.params.appId) { return false; } - if (bid.params.format != 'screen' && bid.params.format != 'display') { + if (typeof bid.params.format != 'string' || typeof bid.params.style != 'string' || !bid.params.format || !bid.params.style) { return false; } - if (bid.params.style != 'inline' && bid.params.style != 'impact' && bid.params.style != 'static') { + if (bid.params.format !== 'screen' && bid.params.format !== 'display') { + return false; + } + if (bid.params.style !== 'inline' && bid.params.style !== 'impact' && bid.params.style !== 'static') { + return false; + } + if (isBanner && (typeof bid.params.size != 'string' || !bid.params.size.includes('x') || bid.params.size.split('x').length != 2)) { return false; } @@ -171,11 +258,20 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { // Create a clean openRTB request let request = createOpenRtbRequest(validBidRequests, bidderRequest); + const imStr = helpers.getImStrFromLocalStorage(); + const options = {} + + if (imStr) { + options.customHeaders = { + 'x-impact': imStr + }; + } return { method: 'POST', - url: ORIGIN + AUCTIONURI, + url: ORIGIN + AUCTION_URI, data: JSON.stringify(request), + options }; }, @@ -265,7 +361,7 @@ export const spec = { return [{ type: 'iframe', - url: ORIGIN + COOKIESYNCURI + params + url: ORIGIN + COOKIE_SYNC_URI + params }]; }, @@ -274,7 +370,7 @@ export const spec = { * @param {Bid} The bid that won the auction */ onBidWon: function(bid) { - ajax(`${LOGGER_URI}/log/bidder/won`, null, JSON.stringify(bid), { + ajax(`${LOGGER_URI}/prebid/won`, null, JSON.stringify(bid), { method: 'POST', contentType: 'application/json' }); @@ -287,7 +383,7 @@ export const spec = { * @param {data} Containing timeout specific data */ onTimeout: function(data) { - ajax(`${LOGGER_URI}/log/bidder/timeout`, null, JSON.stringify(data[0]), { + ajax(`${LOGGER_URI}/prebid/timeout`, null, JSON.stringify(data[0]), { method: 'POST', contentType: 'application/json' }); diff --git a/modules/impactifyBidAdapter.md b/modules/impactifyBidAdapter.md index 3de9a8cfb84..de3373395dc 100644 --- a/modules/impactifyBidAdapter.md +++ b/modules/impactifyBidAdapter.md @@ -10,14 +10,22 @@ Maintainer: thomas.destefano@impactify.io Module that connects to the Impactify solution. The impactify bidder need 3 parameters: - - appId : This is your unique publisher identifier - - format : This is the ad format needed, can be : screen or display - - style : This is the ad style needed, can be : inline, impact or static +- appId : This is your unique publisher identifier +- format : This is the ad format needed, can be : screen or display (Only for video media type) +- style : This is the ad style needed, can be : inline, impact or static (Only for video media type) + +Note : Impactify adapter need storage access to work properly (Do not forget to set storageAllowed to true). # Test Parameters ``` - var adUnits = [{ - code: 'your-slot-div-id', // This is your slot div id + pbjs.bidderSettings = { + impactify: { + storageAllowed: true // Mandatory + } + }; + + var adUnitsVideo = [{ + code: 'your-slot-div-id-video', // This is your slot div id mediaTypes: { video: { context: 'outstream' @@ -32,4 +40,24 @@ The impactify bidder need 3 parameters: } }] }]; + + var adUnitsBanner = [{ + code: 'your-slot-div-id-banner', // This is your slot div id + mediaTypes: { + banner: { + sizes: [ + [728, 90] + ] + } + }, + bids: [{ + bidder: 'impactify', + params: { + appId: 'example.com', + format: 'display', + size: '728x90', + style: 'static' + } + }] + }]; ``` diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index b56cc56a186..b563faf52ac 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -182,6 +182,10 @@ export const CONVERTER = ortbConverter({ })(); const bidResponse = buildBidResponse(bid, context); const idExt = deepAccess(bid, `ext.${BIDDER_CODE}`, {}); + // Programmatic guaranteed flag + if (idExt.pg === 1) { + bidResponse.adserverTargeting = { hb_deal_type_improve: 'pg' }; + } Object.assign(bidResponse, { dealId: (typeof idExt.buying_type === 'string' && idExt.buying_type !== 'rtb') ? idExt.line_item_id : undefined, netRevenue: idExt.is_net || false, diff --git a/modules/imuIdSystem.js b/modules/imuIdSystem.js index 898f32b27b0..38870c9403b 100644 --- a/modules/imuIdSystem.js +++ b/modules/imuIdSystem.js @@ -160,6 +160,16 @@ export const imuIdSubmodule = { imppid: localData.ppid } }; + }, + eids: { + 'imppid': { + source: 'ppid.intimatemerger.com', + atype: 1 + }, + 'imuid': { + source: 'intimatemerger.com', + atype: 1 + }, } }; diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index c770ac69dbe..a18c893b5fc 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -106,8 +106,10 @@ function buildImpression(bidRequest) { return imp; } -function buildDevice() { - const deviceConfig = config.getConfig('device'); +function buildDevice(bidRequest) { + const ortb2Data = bidRequest?.ortb2 || {}; + const deviceConfig = ortb2Data?.device || {} + const device = { w: window.innerWidth, h: window.innerHeight, @@ -184,7 +186,7 @@ function buildRequest(validBidRequests, bidderRequest) { page: bidderRequest.refererInfo.page, ref: bidderRequest.refererInfo.ref, }, - device: buildDevice(), + device: buildDevice(bidderRequest), regs: buildRegs(bidderRequest), user: buildUser(validBidRequests[0]), imp: validBidRequests.map((bidRequest) => buildImpression(bidRequest)), diff --git a/modules/integr8BidAdapter.js b/modules/integr8BidAdapter.js index a85e9b0a55c..cc19daf31a3 100644 --- a/modules/integr8BidAdapter.js +++ b/modules/integr8BidAdapter.js @@ -4,12 +4,12 @@ import { getStorageManager } from '../src/storageManager.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'integr8'; -const ENDPOINT_URL = 'https://integr8.central.gjirafa.tech/bid'; +const DEFAULT_ENDPOINT_URL = 'https://central.sea.integr8.digital/bid'; const DIMENSION_SEPARATOR = 'x'; const SIZE_SEPARATOR = ';'; -const BISKO_ID = 'biskoId'; +const BISKO_ID = 'integr8Id'; const STORAGE_ID = 'bisko-sid'; -const SEGMENTS = 'biskoSegments'; +const SEGMENTS = 'integr8Segments'; const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { @@ -31,6 +31,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + let deliveryUrl = ''; const storageId = storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(STORAGE_ID) || '' : ''; const biskoId = storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(BISKO_ID) || '' : ''; const segments = storage.localStorageIsEnabled() ? JSON.parse(storage.getDataFromLocalStorage(SEGMENTS)) || [] : []; @@ -55,6 +56,9 @@ export const spec = { if (!pageViewGuid) { pageViewGuid = bidRequest.params.pageViewGuid || ''; } if (!contents.length && bidRequest.params.contents && bidRequest.params.contents.length) { contents = bidRequest.params.contents; } if (!Object.keys(data).length && bidRequest.params.data && Object.keys(bidRequest.params.data).length) { data = bidRequest.params.data; } + if (!deliveryUrl && bidRequest.params && typeof bidRequest.params.deliveryUrl === 'string') { + deliveryUrl = bidRequest.params.deliveryUrl; + } return { sizes: generateSizeParam(bidRequest.sizes), @@ -67,6 +71,10 @@ export const spec = { }; }); + if (!deliveryUrl) { + deliveryUrl = DEFAULT_ENDPOINT_URL; + } + let body = { propertyId: propertyId, pageViewGuid: pageViewGuid, @@ -82,7 +90,7 @@ export const spec = { return [{ method: 'POST', - url: ENDPOINT_URL, + url: deliveryUrl, data: body }]; }, diff --git a/modules/integr8BidAdapter.md b/modules/integr8BidAdapter.md index eadab7acdb3..da52a2164c6 100644 --- a/modules/integr8BidAdapter.md +++ b/modules/integr8BidAdapter.md @@ -3,7 +3,7 @@ Module Name: Integr8 Bidder Adapter Module Type: Bidder Adapter -Maintainer: arditb@gjirafa.com +Maintainer: myhedin@gjirafa.com # Description Integr8 Bidder Adapter for Prebid.js. @@ -23,8 +23,9 @@ var adUnits = [ bids: [{ bidder: 'integr8', params: { - propertyId: '105109', //Required - placementId: '846835', //Required + propertyId: '105135', //Required + placementId: '846837', //Required, + deliveryUrl: 'https://central.sea.integr8.digital/bid', //Optional data: { //Optional catalogs: [{ catalogId: "699229", @@ -48,8 +49,9 @@ var adUnits = [ bids: [{ bidder: 'integr8', params: { - propertyId: '105109', //Required - placementId: '846830', //Required + propertyId: '105135', //Required + placementId: '846835', //Required, + deliveryUrl: 'https://central.sea.integr8.digital/bid', //Optional data: { //Optional catalogs: [{ catalogId: "699229", diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 4a6977973f3..5164080c317 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -59,7 +59,7 @@ export function readData(key) { * @param key * @param {string} value IntentIQ ID value to sintentIqIdSystem_spec.jstore */ -function storeData(key, value) { +function storeData(key, value, cookieStorageEnabled = false) { try { logInfo(MODULE_NAME + ': storing data: key=' + key + ' value=' + value); @@ -68,7 +68,7 @@ function storeData(key, value) { storage.setDataInLocalStorage(key, value); } const expiresStr = (new Date(Date.now() + (PCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString(); - if (storage.cookiesAreEnabled()) { + if (storage.cookiesAreEnabled() && cookieStorageEnabled) { storage.setCookie(key, value, expiresStr, 'LAX'); } } @@ -119,6 +119,7 @@ export const intentIqIdSubmodule = { logError('User ID - intentIqId submodule requires a valid partner to be defined'); return; } + const cookieStorageEnabled = typeof configParams.enableCookieStorage === 'boolean' ? configParams.enableCookieStorage : false if (!FIRST_PARTY_DATA_KEY.includes(configParams.partner)) { FIRST_PARTY_DATA_KEY += '_' + configParams.partner; } let rrttStrtTime = 0; @@ -127,7 +128,7 @@ export const intentIqIdSubmodule = { if (!firstPartyData || !firstPartyData.pcid || firstPartyData.pcidDate) { const firstPartyId = generateGUID(); firstPartyData = { 'pcid': firstPartyId, 'pcidDate': Date.now() }; - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData)); + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), cookieStorageEnabled); } let partnerData = tryParse(readData(FIRST_PARTY_DATA_KEY)); @@ -172,8 +173,8 @@ export const intentIqIdSubmodule = { } if (shouldUpdateLs === true) { partnerData.date = Date.now(); - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData)); - storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData)); + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), cookieStorageEnabled); + storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), cookieStorageEnabled); } callback(respJson.data); } else { @@ -192,6 +193,12 @@ export const intentIqIdSubmodule = { } }; return { callback: resp }; + }, + eids: { + 'intentIqId': { + source: 'intentiq.com', + atype: 1 + }, } }; diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index b2444043c22..0c0d1cdef87 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -1,4 +1,4 @@ -import { logInfo } from '../src/utils.js'; +import {logInfo} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -9,7 +9,7 @@ const CONSTANTS = { SYNC_ENDPOINT: 'https://k.r66net.com/GetUserSync', TIME_TO_LIVE: 300, DEFAULT_CURRENCY: 'EUR', - PREBID_VERSION: 9, + PREBID_VERSION: 10, METHOD: 'GET', INVIBES_VENDOR_ID: 436, USERID_PROVIDERS: ['pubcid', 'pubProvidedId', 'uid2', 'zeotapIdPlus', 'id5id'], @@ -116,7 +116,7 @@ function buildRequest(bidRequests, bidderRequest) { bidParamsJson.userId = userIdModel; } let data = { - location: getDocumentLocation(topWin), + location: getDocumentLocation(bidderRequest), videoAdHtmlId: generateRandomId(), showFallback: currentQueryStringParams['advs'] === '0', ivbsCampIdsLocal: readFromLocalStorage('IvbsCampIdsLocal'), @@ -371,8 +371,8 @@ function generateRandomId() { return (Math.round(Math.random() * 1e12)).toString(36).substring(0, 10); } -function getDocumentLocation(topWin) { - return topWin.location.href.substring(0, 300).split(/[?#]/)[0]; +function getDocumentLocation(bidderRequest) { + return bidderRequest.refererInfo.page.substring(0, 300); } function getUserIds(bidUserId) { diff --git a/modules/ivsBidAdapter.js b/modules/ivsBidAdapter.js index 47685fbbe46..6f4c024f09f 100644 --- a/modules/ivsBidAdapter.js +++ b/modules/ivsBidAdapter.js @@ -1,5 +1,5 @@ import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import { deepAccess, deepSetValue, getBidIdParameter, logError } from '../src/utils.js'; +import {deepAccess, deepSetValue, getBidIdParameter, logError} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO } from '../src/mediaTypes.js'; import { INSTREAM } from '../src/video.js'; diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 000131d9e03..41e0d8e97c5 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -1,10 +1,8 @@ import { contains, - convertTypes, deepAccess, deepClone, deepSetValue, - getGptSlotInfoForAdUnitCode, inIframe, isArray, isEmpty, @@ -24,6 +22,8 @@ import { find } from '../src/polyfill.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { INSTREAM, OUTSTREAM } from '../src/video.js'; import { Renderer } from '../src/Renderer.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'ix'; const ALIAS_BIDDER_CODE = 'roundel'; @@ -76,7 +76,12 @@ const SOURCE_RTI_MAPPING = { 'audigent.com': '', // Hadron ID from Audigent, hadronId 'pubcid.org': '', // SharedID, pubcid 'utiq.com': '', // Utiq - 'intimatemerger.com': '' + 'criteo.com': '', // Criteo + 'euid.eu': '', // EUID + 'intimatemerger.com': '', + '33across.com': '', + 'liveintent.indexexchange.com': '', + 'google.com': '' }; const PROVIDERS = [ 'britepoolid', @@ -87,7 +92,8 @@ const PROVIDERS = [ 'connectid', 'tapadId', 'quantcastId', - 'pubProvidedId' + 'pubProvidedId', + 'pairId' ]; const REQUIRED_VIDEO_PARAMS = ['mimes', 'minduration', 'maxduration']; // note: protocol/protocols is also reqd const VIDEO_PARAMS_ALLOW_LIST = [ @@ -104,7 +110,9 @@ let hasRegisteredHandler = false; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const FEATURE_TOGGLES = { // Update with list of CFTs to be requested from Exchange - REQUESTED_FEATURE_TOGGLES: [], + REQUESTED_FEATURE_TOGGLES: [ + 'pbjs_enable_multiformat' + ], featureToggles: {}, isFeatureEnabled: function (ft) { @@ -164,6 +172,7 @@ const MEDIA_TYPES = { function bidToBannerImp(bid) { const imp = bidToImp(bid, BANNER); imp.banner = {}; + imp.adunitCode = bid.adUnitCode; const impSize = deepAccess(bid, 'params.size'); if (impSize) { imp.banner.w = impSize[0]; @@ -177,13 +186,48 @@ function bidToBannerImp(bid) { return imp; } +/** + * Sets imp.displaymanager + * + * @param {object} imp + * @param {object} bid + */ +function setDisplayManager(imp, bid) { + if (deepAccess(bid, 'mediaTypes.video.context') === OUTSTREAM) { + let renderer = deepAccess(bid, 'mediaTypes.video.renderer'); + if (!renderer) { + renderer = deepAccess(bid, 'renderer'); + } + + if (deepAccess(bid, 'schain', false)) { + imp.displaymanager = 'pbjs_wrapper'; + } else if (renderer && typeof (renderer) === 'object') { + if (renderer.url !== undefined) { + let domain = ''; + try { + domain = new URL(renderer.url).hostname + } catch { + return; + } + if (domain.includes('js-sec.indexww')) { + imp.displaymanager = 'ix'; + } else { + imp.displaymanager = renderer.url; + } + } + } else { + imp.displaymanager = 'ix'; + } + } +} + /** * Transform valid bid request config object to video impression object that will be sent to ad server. * * @param {object} bid A valid bid request config object. * @return {object} A impression object that will be sent to ad server. */ -function bidToVideoImp(bid) { +export function bidToVideoImp(bid) { const imp = bidToImp(bid, VIDEO); const videoAdUnitRef = deepAccess(bid, 'mediaTypes.video'); const videoParamRef = deepAccess(bid, 'params.video'); @@ -194,10 +238,15 @@ function bidToVideoImp(bid) { imp.video = videoParamRef ? deepClone(bid.params.video) : {}; // populate imp level transactionId - imp.ext.tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + let tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + if (tid) { + deepSetValue(imp, 'ext.tid', tid); + } + + setDisplayManager(imp, bid); // AdUnit-Specific First Party Data - addAdUnitFPD(imp, bid) + addAdUnitFPD(imp, bid); // copy all video properties to imp object for (const adUnitProperty in videoAdUnitRef) { @@ -266,7 +315,7 @@ function verifyVideoPlcmt(imp) { * @param {object} bid A valid bid request config object. * @return {object} A impression object that will be sent to ad server. */ -function bidToNativeImp(bid) { +export function bidToNativeImp(bid) { const imp = bidToImp(bid, NATIVE); const request = bid.nativeOrtbRequest @@ -282,7 +331,10 @@ function bidToNativeImp(bid) { }; // populate imp level transactionId - imp.ext.tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + let tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + if (tid) { + deepSetValue(imp, 'ext.tid', tid); + } // AdUnit-Specific First Party Data addAdUnitFPD(imp, bid) @@ -302,27 +354,30 @@ function bidToImp(bid, mediaType) { imp.id = bid.bidId; - imp.ext = {}; - + if (isExchangeIdConfigured() && deepAccess(bid, `params.externalId`)) { + deepSetValue(imp, 'ext.externalID', bid.params.externalId); + } if (deepAccess(bid, `params.${mediaType}.siteId`) && !isNaN(Number(bid.params[mediaType].siteId))) { switch (mediaType) { case BANNER: - imp.ext.siteID = bid.params.banner.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.banner.siteId.toString()); break; case VIDEO: - imp.ext.siteID = bid.params.video.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.video.siteId.toString()); break; case NATIVE: - imp.ext.siteID = bid.params.native.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.native.siteId.toString()); break; } } else { - imp.ext.siteID = bid.params.siteId.toString(); + if (bid.params.siteId) { + deepSetValue(imp, 'ext.siteID', bid.params.siteId.toString()); + } } // populate imp level sid if (bid.params.hasOwnProperty('id') && (typeof bid.params.id === 'string' || typeof bid.params.id === 'number')) { - imp.ext.sid = String(bid.params.id); + deepSetValue(imp, 'ext.sid', String(bid.params.id)); } return imp; @@ -364,14 +419,30 @@ function _applyFloor(bid, imp, mediaType) { } // Prioritize module floor over bidder.param floor + let setFloor = false; if (moduleFloor) { imp.bidfloor = moduleFloor.floor; imp.bidfloorcur = moduleFloor.currency; - imp.ext.fl = FLOOR_SOURCE.PBJS; + deepSetValue(imp, 'ext.fl', FLOOR_SOURCE.PBJS); + setFloor = true; } else if (adapterFloor) { imp.bidfloor = adapterFloor.floor; imp.bidfloorcur = adapterFloor.currency; - imp.ext.fl = FLOOR_SOURCE.IX; + deepSetValue(imp, 'ext.fl', FLOOR_SOURCE.IX); + setFloor = true; + } + + if (setFloor) { + if (mediaType == BANNER) { + deepSetValue(imp, 'banner.ext.bidfloor', imp.bidfloor); + deepSetValue(imp, 'banner.ext.fl', imp.ext.fl); + } else if (mediaType == VIDEO) { + deepSetValue(imp, 'video.ext.bidfloor', imp.bidfloor); + deepSetValue(imp, 'video.ext.fl', imp.ext.fl); + } else { + deepSetValue(imp, 'native.ext.bidfloor', imp.bidfloor); + deepSetValue(imp, 'native.ext.fl', imp.ext.fl); + } } } @@ -512,7 +583,6 @@ function checkVideoParams(mediaTypeVideoRef, paramsVideoRef) { if (!(protocolMediaType || protocolsMediaType || protocolVideoRef || protocolsVideoRef)) { errorList.push('IX Bid Adapter: protocol/protcols is not included in either the adunit or params level'); } - return errorList; } @@ -638,7 +708,8 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r = addRequestedFeatureToggles(r, FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES) // getting ixdiags for adunits of the video, outstream & multi format (MF) style - let ixdiag = buildIXDiag(validBidRequests); + const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + let ixdiag = buildIXDiag(validBidRequests, fledgeEnabled); for (var key in ixdiag) { r.ext.ixdiag[key] = ixdiag[key]; } @@ -648,8 +719,10 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r = applyRegulations(r, bidderRequest); let payload = {}; - siteID = validBidRequests[0].params.siteId; - payload.s = siteID; + if (validBidRequests[0].params.siteId) { + siteID = validBidRequests[0].params.siteId; + payload.s = siteID; + } const impKeys = Object.keys(impressions); let isFpdAdded = false; @@ -680,10 +753,24 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { const isLastAdUnit = adUnitIndex === impKeys.length - 1; + r = addDeviceInfo(r); + r = deduplicateImpExtFields(r); + r = removeSiteIDs(r); + if (isLastAdUnit) { + let exchangeUrl = `${baseUrl}?`; + + if (siteID !== 0) { + exchangeUrl += `s=${siteID}`; + } + + if (isExchangeIdConfigured()) { + exchangeUrl += siteID !== 0 ? '&' : ''; + exchangeUrl += `p=${config.getConfig('exchangeId')}`; + } requests.push({ method: 'POST', - url: baseUrl + '?s=' + siteID, + url: exchangeUrl, data: deepClone(r), option: { contentType: 'text/plain', @@ -889,63 +976,135 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { const gpid = impressions[impKeys[adUnitIndex]].gpid; const dfpAdUnitCode = impressions[impKeys[adUnitIndex]].dfp_ad_unit_code; const tid = impressions[impKeys[adUnitIndex]].tid; - const sid = impressions[impKeys[adUnitIndex]].sid - - if (impressionObjects.length && BANNER in impressionObjects[0]) { - const { id, banner: { topframe } } = impressionObjects[0]; - const _bannerImpression = { - id, - banner: { - topframe, - format: impressionObjects.map(({ banner: { w, h }, ext }) => ({ w, h, ext })) - }, - }; + const sid = impressions[impKeys[adUnitIndex]].sid; + const auctionEnvironment = impressions[impKeys[adUnitIndex]].ae; + const bannerImpressions = impressionObjects.filter(impression => BANNER in impression); + const otherImpressions = impressionObjects.filter(impression => !(BANNER in impression)); + + if (bannerImpressions.length > 0) { + const bannerImpsKeyed = bannerImpressions.reduce((acc, bannerImp) => { + if (!acc[bannerImp.adunitCode]) { + acc[bannerImp.adunitCode] = [] + } + acc[bannerImp.adunitCode].push(bannerImp); + return acc; + }, {}); + for (const impId in bannerImpsKeyed) { + const bannerImps = bannerImpsKeyed[impId]; + const { id, banner: { topframe } } = bannerImps[0]; + let externalID = deepAccess(bannerImps[0], 'ext.externalID'); + const _bannerImpression = { + id, + banner: { + topframe, + format: bannerImps.map(({ banner: { w, h }, ext }) => ({ w, h, ext })) + }, + }; + + for (let i = 0; i < _bannerImpression.banner.format.length; i++) { + // We add sid and externalID in imp.ext therefore, remove from banner.format[].ext + if (_bannerImpression.banner.format[i].ext != null) { + if (_bannerImpression.banner.format[i].ext.sid != null) { + delete _bannerImpression.banner.format[i].ext.sid; + } + if (_bannerImpression.banner.format[i].ext.externalID != null) { + delete _bannerImpression.banner.format[i].ext.externalID; + } + } - for (let i = 0; i < _bannerImpression.banner.format.length; i++) { - // We add sid in imp.ext.sid therefore, remove from banner.format[].ext - if (_bannerImpression.banner.format[i].ext != null && _bannerImpression.banner.format[i].ext.sid != null) { - delete _bannerImpression.banner.format[i].ext.sid; + // add floor per size + if ('bidfloor' in bannerImps[i]) { + _bannerImpression.banner.format[i].ext.bidfloor = bannerImps[i].bidfloor; + } + + if (JSON.stringify(_bannerImpression.banner.format[i].ext) === '{}') { + delete _bannerImpression.banner.format[i].ext; + } } - // add floor per size - if ('bidfloor' in impressionObjects[i]) { - _bannerImpression.banner.format[i].ext.bidfloor = impressionObjects[i].bidfloor; + const position = impressions[impKeys[adUnitIndex]].pos; + if (isInteger(position)) { + _bannerImpression.banner.pos = position; } - } - const position = impressions[impKeys[adUnitIndex]].pos; - if (isInteger(position)) { - _bannerImpression.banner.pos = position; - } + if (dfpAdUnitCode || gpid || tid || sid || auctionEnvironment || externalID) { + _bannerImpression.ext = {}; - if (dfpAdUnitCode || gpid || tid || sid) { - _bannerImpression.ext = {}; - _bannerImpression.ext.dfp_ad_unit_code = dfpAdUnitCode; - _bannerImpression.ext.gpid = gpid; - _bannerImpression.ext.tid = tid; - _bannerImpression.ext.sid = sid; - } + _bannerImpression.ext.dfp_ad_unit_code = dfpAdUnitCode; + _bannerImpression.ext.gpid = gpid; + _bannerImpression.ext.tid = tid; + _bannerImpression.ext.sid = sid; + _bannerImpression.ext.externalID = externalID; - if ('bidfloor' in impressionObjects[0]) { - _bannerImpression.bidfloor = impressionObjects[0].bidfloor; - } + // enable fledge auction + if (auctionEnvironment == 1) { + _bannerImpression.ext.ae = 1; + } + } - if ('bidfloorcur' in impressionObjects[0]) { - _bannerImpression.bidfloorcur = impressionObjects[0].bidfloorcur; - } + if ('bidfloor' in bannerImps[0]) { + _bannerImpression.bidfloor = bannerImps[0].bidfloor; + } - const adUnitFPD = impressions[impKeys[adUnitIndex]].adUnitFPD - if (adUnitFPD) { - _bannerImpression.ext.data = adUnitFPD; - } + if ('bidfloorcur' in bannerImps[0]) { + _bannerImpression.bidfloorcur = bannerImps[0].bidfloorcur; + } - r.imp.push(_bannerImpression); - } else { - // set imp.ext.gpid to resolved gpid for each imp - impressionObjects.forEach(imp => deepSetValue(imp, 'ext.gpid', gpid)); - r.imp.push(...impressionObjects); + const adUnitFPD = impressions[impKeys[adUnitIndex]].adUnitFPD + if (adUnitFPD) { + deepSetValue(_bannerImpression, 'ext.data', adUnitFPD); + } + r.imp.push(_bannerImpression); + } + } + if (otherImpressions.length > 0) { + // Creates multiformat imp if they have the same ID + // if not same ID, just add the imp to the imp array + // Removes imp.ext.bidfloor + // Sets imp.ext.siteID to one of the other [video/native].ext.siteid if imp.ext.siteID doesnt exist + otherImpressions.forEach(imp => { + if (gpid) { + deepSetValue(imp, 'ext.gpid', gpid); + } + if (r.imp.length > 0) { + let matchFound = false; + r.imp.forEach((rImp, index) => { + if (imp.id === rImp.id && VIDEO in imp) { + rImp.video = imp.video; + rImp.video.ext = Object.assign({}, imp.video.ext, imp.ext); + if (deepAccess(rImp, 'video.ext.bidfloor', false) && deepAccess(rImp, 'bidfloor', false)) { + if (rImp.video.ext.bidfloor < rImp.bidfloor) { + rImp.bidfloor = rImp.video.ext.bidfloor; + } + } + if (!deepAccess(rImp, 'ext.siteID', false) && deepAccess(imp, 'video.ext.siteID')) { + deepSetValue(rImp, 'ext.siteID', imp.video.ext.siteID); + deepSetValue(r, 'ext.ixdiag.usid', true); + } + matchFound = true; + } else if (imp.id === rImp.id && NATIVE in imp) { + rImp.native = imp.native; + rImp.native.ext = Object.assign({}, imp.native.ext, imp.ext); + if (deepAccess(rImp, 'native.ext.bidfloor', false) && deepAccess(rImp, 'bidfloor', false)) { + if (rImp.native.ext.bidfloor < rImp.bidfloor) { + rImp.bidfloor = rImp.native.ext.bidfloor; + } + } + if (!deepAccess(rImp, 'ext.siteID', false) && deepAccess(imp, 'native.ext.siteID', false)) { + deepSetValue(rImp, 'ext.siteID', imp.native.ext.siteID); + deepSetValue(r, 'ext.ixdiag.usid', true); + } + matchFound = true; + } + }); + if (!matchFound) { + r.imp.push(imp); + } + } else { + r.imp.push(imp); + } + }); } - return r; } @@ -1106,15 +1265,16 @@ function _getUserIds(bidRequest) { /** * Calculates IX diagnostics values and packages them into an object * - * @param {array} validBidRequests The valid bid requests from prebid + * @param {array} validBidRequests - The valid bid requests from prebid + * @param {bool} fledgeEnabled - Flag indicating if protected audience (fledge) is enabled * @return {Object} IX diag values for ad units */ -function buildIXDiag(validBidRequests) { +function buildIXDiag(validBidRequests, fledgeEnabled) { var adUnitMap = validBidRequests .map(bidRequest => bidRequest.adUnitCode) .filter((value, index, arr) => arr.indexOf(value) === index); - var ixdiag = { + let ixdiag = { mfu: 0, bu: 0, iu: 0, @@ -1125,12 +1285,13 @@ function buildIXDiag(validBidRequests) { version: '$prebid.version$', userIds: _getUserIds(validBidRequests[0]), url: window.location.href.split('?')[0], - vpd: defaultVideoPlacement + vpd: defaultVideoPlacement, + ae: fledgeEnabled }; // create ad unit map and collect the required diag properties - for (let i = 0; i < adUnitMap.length; i++) { - var bid = validBidRequests.filter(bidRequest => bidRequest.adUnitCode === adUnitMap[i])[0]; + for (let adUnit of adUnitMap) { + let bid = validBidRequests.filter(bidRequest => bidRequest.adUnitCode === adUnit)[0]; if (deepAccess(bid, 'mediaTypes')) { if (Object.keys(bid.mediaTypes).length > 1) { @@ -1236,7 +1397,7 @@ function createVideoImps(validBidRequest, videoImps) { * @param {object} missingBannerSizes reference to missing banner config sizes * @param {object} bannerImps reference to created banner impressions */ -function createBannerImps(validBidRequest, missingBannerSizes, bannerImps) { +function createBannerImps(validBidRequest, missingBannerSizes, bannerImps, bidderRequest) { let imp = bidToBannerImp(validBidRequest); const bannerSizeDefined = includesSize(deepAccess(validBidRequest, 'mediaTypes.banner.sizes'), deepAccess(validBidRequest, 'params.size')); @@ -1252,6 +1413,21 @@ function createBannerImps(validBidRequest, missingBannerSizes, bannerImps) { bannerImps[validBidRequest.adUnitCode].tagId = deepAccess(validBidRequest, 'params.tagId'); bannerImps[validBidRequest.adUnitCode].pos = deepAccess(validBidRequest, 'mediaTypes.banner.pos'); + // Add Fledge flag if enabled + const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + if (fledgeEnabled) { + const auctionEnvironment = deepAccess(validBidRequest, 'ortb2Imp.ext.ae') + if (auctionEnvironment) { + if (isInteger(auctionEnvironment)) { + bannerImps[validBidRequest.adUnitCode].ae = auctionEnvironment; + } else { + logWarn('error setting auction environment flag - must be an integer') + } + } else if (deepAccess(bidderRequest, 'defaultForSlots') == 1) { + bannerImps[validBidRequest.adUnitCode].ae = 1 + } + } + // AdUnit-Specific First Party Data const adUnitFPD = deepAccess(validBidRequest, 'ortb2Imp.ext.data'); if (adUnitFPD) { @@ -1487,6 +1663,17 @@ function isIndexRendererPreferred(bid) { return !isValid || renderer.backupOnly; } +function isExchangeIdConfigured() { + let exchangeId = config.getConfig('exchangeId'); + if (typeof exchangeId === 'number' && isFinite(exchangeId)) { + return true; + } + if (typeof exchangeId === 'string' && exchangeId.trim() !== '' && isFinite(Number(exchangeId))) { + return true; + } + return false; +} + export const spec = { code: BIDDER_CODE, @@ -1544,14 +1731,21 @@ export const spec = { } } - if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { - logError('IX Bid Adapter: siteId must be string or number type.', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + if (!isExchangeIdConfigured() && bid.params.siteId == undefined) { + logError('IX Bid Adapter: Invalid configuration - either siteId or exchangeId must be configured.'); return false; } - if (typeof bid.params.siteId !== 'string' && isNaN(Number(bid.params.siteId))) { - logError('IX Bid Adapter: siteId must valid value', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); - return false; + if (bid.params.siteId !== undefined) { + if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { + logError('IX Bid Adapter: siteId must be string or number type.', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + return false; + } + + if (typeof bid.params.siteId !== 'string' && isNaN(Number(bid.params.siteId))) { + logError('IX Bid Adapter: siteId must valid value', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + return false; + } } if (hasBidFloor || hasBidFloorCur) { @@ -1584,6 +1778,11 @@ export const spec = { return nativeMediaTypeValid(bid); }, + // For testing only - resets the siteID to 0 so that it can be set again + resetSiteID: function () { + siteID = 0; + }, + /** * Make a server request from the list of BidRequests. * @@ -1606,7 +1805,7 @@ export const spec = { for (const type in adUnitMediaTypes) { switch (adUnitMediaTypes[type]) { case BANNER: - createBannerImps(validBidRequest, missingBannerSizes, bannerImps); + createBannerImps(validBidRequest, missingBannerSizes, bannerImps, bidderRequest); break; case VIDEO: createVideoImps(validBidRequest, videoImps) @@ -1642,17 +1841,31 @@ export const spec = { } } - // Step 4: Build banner, video & native requests + // Step 3: Build banner, video & native requests + let allImps = []; if (Object.keys(bannerImps).length > 0) { - reqs.push(...buildRequest(validBidRequests, bidderRequest, bannerImps, BANNER_ENDPOINT_VERSION)); + allImps.push(bannerImps); } if (Object.keys(videoImps).length > 0) { - reqs.push(...buildRequest(validBidRequests, bidderRequest, videoImps, VIDEO_ENDPOINT_VERSION)); + allImps.push(videoImps); } if (Object.keys(nativeImps).length > 0) { - reqs.push(...buildRequest(validBidRequests, bidderRequest, nativeImps)); + allImps.push(nativeImps); } + if (FEATURE_TOGGLES.isFeatureEnabled('pbjs_enable_multiformat')) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, combineImps(allImps))); + } else { + if (Object.keys(bannerImps).length > 0) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, bannerImps, BANNER_ENDPOINT_VERSION)); + } + if (Object.keys(videoImps).length > 0) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, videoImps, VIDEO_ENDPOINT_VERSION)); + } + if (Object.keys(nativeImps).length > 0) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, nativeImps)); + } + } return reqs; }, @@ -1667,13 +1880,18 @@ export const spec = { const bids = []; let bid = null; - if (!serverResponse.hasOwnProperty('body') || !serverResponse.body.hasOwnProperty('seatbid')) { - FEATURE_TOGGLES.setFeatureToggles(serverResponse); + // Extract the FLEDGE auction configuration list from the response + let fledgeAuctionConfigs = deepAccess(serverResponse, 'body.ext.protectedAudienceAuctionConfigs') || []; + + FEATURE_TOGGLES.setFeatureToggles(serverResponse); + + if (!serverResponse.hasOwnProperty('body')) { return bids; } const responseBody = serverResponse.body; - const seatbid = responseBody.seatbid; + const seatbid = responseBody.seatbid || []; + for (let i = 0; i < seatbid.length; i++) { if (!seatbid[i].hasOwnProperty('bid')) { continue; @@ -1709,8 +1927,28 @@ export const spec = { } } - FEATURE_TOGGLES.setFeatureToggles(serverResponse); - return bids; + if (Array.isArray(fledgeAuctionConfigs) && fledgeAuctionConfigs.length > 0) { + // Validate and filter fledgeAuctionConfigs + fledgeAuctionConfigs = fledgeAuctionConfigs.filter(config => { + if (!isValidAuctionConfig(config)) { + logWarn('Malformed auction config detected:', config); + return false; + } + return true; + }); + + try { + return { + bids, + fledgeAuctionConfigs, + }; + } catch (error) { + logWarn('Error attaching AuctionConfigs', error); + return bids; + } + } else { + return bids; + } }, /** @@ -1784,8 +2022,176 @@ function buildImgSyncUrl(syncsPerBidder, index) { if (gdprConsent && gdprConsent.hasOwnProperty('consentString')) { consentString = gdprConsent.consentString || ''; } + let siteIdParam = siteID !== 0 ? '&site_id=' + siteID.toString() : ''; - return IMG_USER_SYNC_URL + '&site_id=' + siteID.toString() + '&p=' + syncsPerBidder.toString() + '&i=' + index.toString() + '&gdpr=' + gdprApplies + '&gdpr_consent=' + consentString + '&us_privacy=' + (usPrivacy || ''); + return IMG_USER_SYNC_URL + siteIdParam + '&p=' + syncsPerBidder.toString() + '&i=' + index.toString() + '&gdpr=' + gdprApplies + '&gdpr_consent=' + consentString + '&us_privacy=' + (usPrivacy || ''); +} + +/** + * Combines all imps into a single object + * @param {array} imps array of imps + * @returns object + */ +export function combineImps(imps) { + const result = {} + imps.forEach((imp) => { + Object.keys(imp).forEach((key) => { + if (Object.keys(result).includes(key)) { + if (result[key].hasOwnProperty('ixImps') && imp[key].hasOwnProperty('ixImps')) { + result[key].ixImps = [...result[key].ixImps, ...imp[key].ixImps]; + } else if (result[key].hasOwnProperty('missingImps') && imp[key].hasOwnProperty('missingImps')) { + result[key].missingImps = [...result[key].missingImps, ...imp[key].missingImps]; + } else if (imp[key].hasOwnProperty('ixImps')) { + result[key].ixImps = imp[key].ixImps; + } else if (imp[key].hasOwnProperty('missingImps')) { + result[key].missingImps = imp[key].missingImps + } + } else { + result[key] = imp[key]; + } + }); + }); + return result; +} + +/** + * Deduplicates ext fields. For example if imp.ext.tid exists, removes imp.banner.ext.tid + * + * @param {object} r request object + * @returns object + */ +export function deduplicateImpExtFields(r) { + r.imp.forEach((imp, index) => { + const impExt = imp.ext; + if (impExt == undefined) { + return r; + } + if (getFormatCount(imp) < 2) { + return; + } + Object.keys(impExt).forEach((key) => { + if (BANNER in imp) { + const bannerExt = imp.banner.ext; + if (bannerExt !== undefined && bannerExt[key] !== undefined && bannerExt[key] == impExt[key]) { + delete r.imp[index].banner.ext[key]; + } + if (imp.banner.format !== undefined) { + for (let i = 0; i < imp.banner.format.length; i++) { + if (imp.banner.format[i].ext != undefined && imp.banner.format[i].ext[key] != undefined && imp.banner.format[i].ext[key] == impExt[key]) { + delete r.imp[index].banner.format[i].ext[key]; + } + } + } + } + if (VIDEO in imp) { + const videoExt = imp.video.ext; + if (videoExt !== undefined && videoExt[key] !== undefined && videoExt[key] == impExt[key]) { + delete r.imp[index].video.ext[key]; + } + } + + if (NATIVE in imp) { + const nativeExt = imp.native.ext; + if (nativeExt !== undefined && nativeExt[key] !== undefined && nativeExt[key] == impExt[key]) { + delete r.imp[index].native.ext[key]; + } + } + }); + }); + return r; +} + +/** + * Removes ext.siteids in multiformat scenario + * Site id will be set only at imp.ext.siteId + * + * @param {object} r request object + * @returns object + */ +export function removeSiteIDs(r) { + r.imp.forEach((imp, index) => { + const impExt = imp.ext; + if (impExt == undefined) { + return r; + } + if (getFormatCount(imp) < 2) { + return; + } + + if (BANNER in imp) { + const bannerExt = imp.banner.ext; + if (bannerExt !== undefined && bannerExt.siteID !== undefined) { + delete r.imp[index].banner.ext.siteID; + } + if (imp.banner.format !== undefined) { + for (let i = 0; i < imp.banner.format.length; i++) { + if (imp.banner.format[i].ext !== undefined && imp.banner.format[i].ext.siteID !== undefined) { + deepSetValue(r.imp[index], 'ext.siteID', imp.banner.format[i].ext.siteID); + deepSetValue(r, 'ext.ixdiag.usid', true); + delete r.imp[index].banner.format[i].ext.siteID; + } + } + } + } + + if (VIDEO in imp) { + const videoExt = imp.video.ext; + if (videoExt !== undefined && videoExt.siteID !== undefined) { + delete r.imp[index].video.ext.siteID; + } + } + + if (NATIVE in imp) { + const nativeExt = imp.native.ext; + if (nativeExt !== undefined && nativeExt.siteID !== undefined) { + delete r.imp[index].native.ext.siteID; + } + } + }); + return r; +} + +/** + * Gets count of banner/video/native formats in imp + * @param {object} imp + * @returns int + */ +function getFormatCount(imp) { + let formatCount = 0; + if (imp.banner !== undefined) { + formatCount += 1; + } + if (imp.video !== undefined) { + formatCount += 1; + } + if (imp.native !== undefined) { + formatCount += 1; + } + return formatCount; +} + +/** + * Checks if auction config is valid + * @param {object} config + * @returns bool + */ +function isValidAuctionConfig(config) { + return typeof config === 'object' && config !== null; +} + +/** + * Adds device.w / device.h info + * @param {object} r + * @returns object + */ +export function addDeviceInfo(r) { + if (r.device == undefined) { + r.device = {}; + } + r.device.h = window.screen.height; + r.device.w = window.screen.width; + + return r; } registerBidder(spec); diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index 638cb11c5ab..0705c5932cf 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -469,6 +469,11 @@ pbjs.setConfig({ The timeout value must be a positive whole number in milliseconds. +Protected Audience API (FLEDGE) +=========================== + +In order to enable receiving [Protected Audience API](https://developer.chrome.com/en/docs/privacy-sandbox/fledge/) traffic, follow Prebid's documentation on [fledgeForGpt](https://docs.prebid.org/dev-docs/modules/fledgeForGpt.html) module to build and enable Fledge. + Additional Information ====================== diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index b587011c748..75268e9d168 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, getDNT, isArray, logWarn} from '../src/utils.js'; +import {deepAccess, getDNT, isArray, logWarn, isFn, isPlainObject, logError, logInfo} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -7,13 +7,36 @@ import {ajax} from '../src/ajax.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {Renderer} from '../src/Renderer.js'; +const ADAPTER_VERSION = '2.1.0'; +const PREBID_VERSION = '$prebid.version$'; + const BIDDER_CODE = 'jixie'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); -const EVENTS_URL = 'https://hbtra.jixie.io/sync/hb?'; const JX_OUTSTREAM_RENDERER_URL = 'https://scripts.jixie.media/jxhbrenderer.1.1.min.js'; const REQUESTS_URL = 'https://hb.jixie.io/v2/hbpost'; const sidTTLMins_ = 30; +/** + * Get bid floor from Price Floors Module + * + * @param {Object} bid + * @returns {float||null} + */ +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return null; + } + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + /** * Own miscellaneous support functions: */ @@ -38,12 +61,24 @@ function setIds_(clientId, sessionId) { } catch (error) {} } -function fetchIds_() { +/** + * fetch some ids from cookie, LS. + * @returns + */ +const defaultGenIds_ = [ + { id: '_jxtoko' }, + { id: '_jxifo' }, + { id: '_jxtdid' }, + { id: '_jxcomp' } +]; + +function fetchIds_(cfg) { let ret = { client_id_c: '', client_id_ls: '', session_id_c: '', - session_id_ls: '' + session_id_ls: '', + jxeids: {} }; try { let tmp = storage.getCookie('_jxx'); @@ -55,8 +90,12 @@ function fetchIds_() { if (tmp) ret.client_id_ls = tmp; tmp = storage.getDataFromLocalStorage('_jxxs'); if (tmp) ret.session_id_ls = tmp; - tmp = storage.getCookie('_jxtoko'); - if (tmp) ret.jxtoko_id = tmp; + + let arr = cfg.genids ? cfg.genids : defaultGenIds_; + arr.forEach(function(o) { + tmp = storage.getCookie(o.ck ? o.ck : o.id); + if (tmp) ret.jxeids[o.id] = tmp; + }); } catch (error) {} return ret; } @@ -73,14 +112,6 @@ function getDevice_() { return device; } -function pingTracking_(endpointOverride, qpobj) { - internal.ajax((endpointOverride || EVENTS_URL), null, qpobj, { - withCredentials: true, - method: 'GET', - crossOrigin: true - }); -} - function jxOutstreamRender_(bidAd) { bidAd.renderer.push(() => { window.JixieOutstreamVideo.init({ @@ -132,17 +163,6 @@ function getMiscDims_() { return ret; } -/* function addUserId(eids, id, source, rti) { - if (id) { - if (rti) { - eids.push({ source, id, rti_partner: rti }); - } else { - eids.push({ source, id }); - } - } - return eids; -} */ - // easier for replacement in the unit test export const internal = { getDevice: getDevice_, @@ -153,7 +173,6 @@ export const internal = { export const spec = { code: BIDDER_CODE, - EVENTS_URL: EVENTS_URL, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bid) { if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { @@ -170,39 +189,38 @@ export const spec = { let bids = []; validBidRequests.forEach(function(one) { - bids.push({ + let gpid = deepAccess(one, 'ortb2Imp.ext.gpid', deepAccess(one, 'ortb2Imp.ext.data.pbadslot', '')); + let tmp = { bidId: one.bidId, adUnitCode: one.adUnitCode, mediaTypes: (one.mediaTypes === 'undefined' ? {} : one.mediaTypes), sizes: (one.sizes === 'undefined' ? [] : one.sizes), params: one.params, - }); + gpid: gpid + }; + let bidFloor = getBidFloor(one); + if (bidFloor) { + tmp.bidFloor = bidFloor; + } + bids.push(tmp); }); + let jxCfg = config.getConfig('jixie') || {}; - let jixieCfgBlob = config.getConfig('jixie'); - if (!jixieCfgBlob) { - jixieCfgBlob = {}; - } - - let ids = fetchIds_(); + let ids = fetchIds_(jxCfg); let eids = []; let miscDims = internal.getMiscDims(); let schain = deepAccess(validBidRequests[0], 'schain'); - let eids1 = validBidRequests[0].userIdAsEids + let eids1 = validBidRequests[0].userIdAsEids; // all available user ids are sent to our backend in the standard array layout: if (eids1 && eids1.length) { eids = eids1; } // we want to send this blob of info to our backend: - let pg = config.getConfig('priceGranularity'); - if (!pg) { - pg = {}; - } - let transformedParams = Object.assign({}, { // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionid: bidderRequest.auctionId, + auctionid: bidderRequest.auctionId || '', + aid: jxCfg.aid || '', timeout: bidderRequest.timeout, currency: currency, timestamp: (new Date()).getTime(), @@ -213,8 +231,10 @@ export const spec = { bids: bids, eids: eids, schain: schain, - pricegranularity: pg, - cfg: jixieCfgBlob + pricegranularity: (config.getConfig('priceGranularity') || {}), + ver: ADAPTER_VERSION, + pbjsver: PREBID_VERSION, + cfg: jxCfg }, ids); return Object.assign({}, { method: 'POST', @@ -225,48 +245,20 @@ export const spec = { }, onTimeout: function(timeoutData) { - let jxCfgBlob = config.getConfig('jixie'); - if (jxCfgBlob && jxCfgBlob.onTimeout == 'off') { - return; - } - let url = null;// default - if (jxCfgBlob && jxCfgBlob.onTimeoutUrl && typeof jxCfgBlob.onTimeoutUrl == 'string') { - url = jxCfgBlob.onTimeoutUrl; - } - let miscDims = internal.getMiscDims(); - pingTracking_(url, // no overriding ping URL . just use default - { - action: 'hbtimeout', - device: miscDims.device, - pageurl: encodeURIComponent(miscDims.pageurl), - domain: encodeURIComponent(miscDims.domain), - auctionid: deepAccess(timeoutData, '0.auctionId'), - timeout: deepAccess(timeoutData, '0.timeout'), - count: timeoutData.length - }); + logError('jixie adapter timed out for the auction.', timeoutData); }, onBidWon: function(bid) { - if (bid.notrack) { - return; - } if (bid.trackingUrl) { - pingTracking_(bid.trackingUrl, {}); - } else { - let miscDims = internal.getMiscDims(); - pingTracking_((bid.trackingUrlBase ? bid.trackingUrlBase : null), { - action: 'hbbidwon', - device: miscDims.device, - pageurl: encodeURIComponent(miscDims.pageurl), - domain: encodeURIComponent(miscDims.domain), - cid: bid.cid, - cpid: bid.cpid, - jxbidid: bid.jxBidId, - auctionid: bid.auctionId, - cpm: bid.cpm, - requestid: bid.requestId + internal.ajax(bid.trackingUrl, null, {}, { + withCredentials: true, + method: 'GET', + crossOrigin: true }); } + logInfo( + `jixie adapter won the auction. Bid id: ${bid.bidId}, Ad Unit Id: ${bid.adUnitId}` + ); }, interpretResponse: function(response, bidRequest) { @@ -274,7 +266,6 @@ export const spec = { const bidResponses = []; response.body.bids.forEach(function(oneBid) { let bnd = {}; - Object.assign(bnd, oneBid); if (oneBid.osplayer) { bnd.adResponse = { diff --git a/modules/justIdSystem.js b/modules/justIdSystem.js index 15b1c90da4e..26e9275bbab 100644 --- a/modules/justIdSystem.js +++ b/modules/justIdSystem.js @@ -91,6 +91,12 @@ export const justIdSubmodule = { } } }; + }, + eids: { + 'justId': { + source: 'justtag.com', + atype: 1 + }, } }; diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 5b315ef5791..9d8c7bc06a1 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -24,6 +24,7 @@ const CURRENCY = Object.freeze({ }); const REQUEST_KEYS = Object.freeze({ + USER_DATA: 'ortb2.user.data', SOCIAL_CANVAS: 'params.socialCanvas', SUA: 'ortb2.device.sua', TDID_ADAPTER: 'userId.tdid', @@ -97,11 +98,26 @@ function buildRequests(validBidRequests, bidderRequest) { user: getUserIds(tdidAdapter, bidderRequest.uspConsent, bidderRequest.gdprConsent, firstBidRequest.userIdAsEids, bidderRequest.gppConsent), }); + if (firstBidRequest.ortb2 != null) { + krakenParams.site = { + cat: firstBidRequest.ortb2.site.cat + } + } + + // Add schain + if (firstBidRequest.schain && firstBidRequest.schain.nodes) { + krakenParams.schain = firstBidRequest.schain + } + + // Add user data object if available + krakenParams.user.data = deepAccess(firstBidRequest, REQUEST_KEYS.USER_DATA) || []; + const reqCount = getRequestCount() if (reqCount != null) { krakenParams.requestCount = reqCount; } + // Add currency if not USD if (currency != null && currency != CURRENCY.US_DOLLAR) { krakenParams.cur = currency; } @@ -459,8 +475,8 @@ function getImpression(bid) { imp.bidderWinCount = bid.bidderWinsCount; } - const gpid = getGPID(bid) - if (gpid != null && gpid != '') { + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { imp.fpd = { gpid: gpid } @@ -483,29 +499,6 @@ function getImpression(bid) { return imp } -function getGPID(bid) { - if (bid.ortb2Imp != null) { - if (bid.ortb2Imp.gpid != null && bid.ortb2Imp.gpid != '') { - return bid.ortb2Imp.gpid; - } - - if (bid.ortb2Imp.ext != null && bid.ortb2Imp.ext.data != null) { - if (bid.ortb2Imp.ext.data.pbAdSlot != null && bid.ortb2Imp.ext.data.pbAdSlot != '') { - return bid.ortb2Imp.ext.data.pbAdSlot; - } - - if (bid.ortb2Imp.ext.data.adServer != null && bid.ortb2Imp.ext.data.adServer.adSlot != null && bid.ortb2Imp.ext.data.adServer.adSlot != '') { - return bid.ortb2Imp.ext.data.adServer.adSlot; - } - } - } - - if (bid.adUnitCode != null && bid.adUnitCode != '') { - return bid.adUnitCode; - } - return ''; -} - export const spec = { gvlid: BIDDER.GVLID, code: BIDDER.CODE, diff --git a/modules/kinessoIdSystem.js b/modules/kinessoIdSystem.js index 632f3a669aa..c13ed3976d3 100644 --- a/modules/kinessoIdSystem.js +++ b/modules/kinessoIdSystem.js @@ -233,8 +233,13 @@ export const kinessoIdSubmodule = { const payloadString = JSON.stringify(kinessoIdPayload); ajax(kinessoSyncUrl(accountId, consentData), syncId(knnsoId), payloadString, {method: 'POST', withCredentials: true}); return {'id': knnsoId}; - } - + }, + eids: { + 'kpuid': { + source: 'kpuid.com', + atype: 3 + }, + }, }; // Register submodule for userId diff --git a/modules/kueezBidAdapter.js b/modules/kueezBidAdapter.js index 0a868661310..5a5536e0c1a 100644 --- a/modules/kueezBidAdapter.js +++ b/modules/kueezBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; diff --git a/modules/kueezRtbBidAdapter.js b/modules/kueezRtbBidAdapter.js index bb0534d6372..9a336b16136 100644 --- a/modules/kueezRtbBidAdapter.js +++ b/modules/kueezRtbBidAdapter.js @@ -251,13 +251,20 @@ function interpretResponse(serverResponse, request) { } } -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { let syncs = []; const {iframeEnabled, pixelEnabled} = syncOptions; const {gdprApplies, consentString = ''} = gdprConsent; + const {gppString, applicableSections} = gppConsent; const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + + if (gppString && applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppString); + params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); + } + if (iframeEnabled) { syncs.push({ type: 'iframe', diff --git a/modules/kulturemediaBidAdapter.js b/modules/kulturemediaBidAdapter.js deleted file mode 100644 index fb3f6e4e231..00000000000 --- a/modules/kulturemediaBidAdapter.js +++ /dev/null @@ -1,472 +0,0 @@ -import { - deepAccess, - deepSetValue, - isArray, - isFn, - isNumber, - isPlainObject, - isStr, - logError, - logInfo, - logMessage -} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; - -const BIDDER_CODE = 'kulturemedia'; -const DEFAULT_BID_TTL = 300; -const DEFAULT_CURRENCY = 'USD'; -const DEFAULT_NET_REVENUE = true; -const DEFAULT_NETWORK_ID = 1; -const OPENRTB_VIDEO_PARAMS = [ - 'mimes', - 'minduration', - 'maxduration', - 'placement', - 'protocols', - 'startdelay', - 'skip', - 'skipafter', - 'minbitrate', - 'maxbitrate', - 'delivery', - 'playbackmethod', - 'api', - 'linearity' -]; - -export const spec = { - code: BIDDER_CODE, - VERSION: '1.0.0', - supportedMediaTypes: [BANNER, VIDEO], - ENDPOINT: 'https://ads.kulture.media/pbjs', - - /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bidRequest The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: function (bid) { - return ( - _validateParams(bid) && - _validateBanner(bid) && - _validateVideo(bid) - ); - }, - - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests A non-empty list of bid requests which should be sent to the Server. - * @param {BidderRequest} bidderRequest bidder request object. - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: function (validBidRequests, bidderRequest) { - if (!validBidRequests || !bidderRequest) { - return; - } - - // We need to refactor this to support mixed content when there are both - // banner and video bid requests - let openrtbRequest; - if (hasBannerMediaType(validBidRequests[0])) { - openrtbRequest = buildBannerRequestData(validBidRequests, bidderRequest); - } else if (hasVideoMediaType(validBidRequests[0])) { - openrtbRequest = buildVideoRequestData(validBidRequests[0], bidderRequest); - } - - // adding schain object - if (validBidRequests[0].schain) { - deepSetValue(openrtbRequest, 'source.ext.schain', validBidRequests[0].schain); - } - - // Attaching GDPR Consent Params - if (bidderRequest.gdprConsent) { - deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); - } - - // CCPA - if (bidderRequest.uspConsent) { - deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - // EIDS - const eids = deepAccess(validBidRequests[0], 'userIdAsEids'); - if (Array.isArray(eids) && eids.length > 0) { - deepSetValue(openrtbRequest, 'user.ext.eids', eids); - } - - let publisherId = validBidRequests[0].params.publisherId; - let placementId = validBidRequests[0].params.placementId; - const networkId = validBidRequests[0].params.networkId || DEFAULT_NETWORK_ID; - - if (validBidRequests[0].params.e2etest) { - logMessage('E2E test mode enabled'); - publisherId = 'e2etest' - } - let baseEndpoint = spec.ENDPOINT + '?pid=' + publisherId; - - if (placementId) { - baseEndpoint += '&placementId=' + placementId - } - if (networkId) { - baseEndpoint += '&nId=' + networkId - } - - const payloadString = JSON.stringify(openrtbRequest); - return { - method: 'POST', - url: baseEndpoint, - data: payloadString, - }; - }, - - interpretResponse: function (serverResponse) { - const bidResponses = []; - const response = (serverResponse || {}).body; - // response is always one seat (exchange) with (optional) bids for each impression - if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) { - response.seatbid[0].bid.forEach(bid => { - if (bid.adm && bid.price) { - bidResponses.push(_createBidResponse(bid)); - } - }) - } else { - logInfo('kulturemedia.interpretResponse :: no valid responses to interpret'); - } - return bidResponses; - }, - - getUserSyncs: function (syncOptions, serverResponses) { - logInfo('kulturemedia.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses); - let syncs = []; - - if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { - return syncs; - } - - serverResponses.forEach(resp => { - const userSync = deepAccess(resp, 'body.ext.usersync'); - if (userSync) { - let syncDetails = []; - Object.keys(userSync).forEach(key => { - const value = userSync[key]; - if (value.syncs && value.syncs.length) { - syncDetails = syncDetails.concat(value.syncs); - } - }); - syncDetails.forEach(syncDetails => { - syncs.push({ - type: syncDetails.type === 'iframe' ? 'iframe' : 'image', - url: syncDetails.url - }); - }); - - if (!syncOptions.iframeEnabled) { - syncs = syncs.filter(s => s.type !== 'iframe') - } - if (!syncOptions.pixelEnabled) { - syncs = syncs.filter(s => s.type !== 'image') - } - } - }); - logInfo('kulturemedia.getUserSyncs result=%o', syncs); - return syncs; - }, - -}; - -/* ======================================= - * Util Functions - *======================================= */ - -/** - * @param {BidRequest} bidRequest bid request - */ -function hasBannerMediaType(bidRequest) { - return !!deepAccess(bidRequest, 'mediaTypes.banner'); -} - -/** - * @param {BidRequest} bidRequest bid request - */ -function hasVideoMediaType(bidRequest) { - return !!deepAccess(bidRequest, 'mediaTypes.video'); -} - -function _validateParams(bidRequest) { - if (!bidRequest.params) { - return false; - } - - if (bidRequest.params.e2etest) { - return true; - } - - if (!bidRequest.params.publisherId) { - logError('Validation failed: publisherId not declared'); - return false; - } - - if (!bidRequest.params.placementId) { - logError('Validation failed: placementId not declared'); - return false; - } - - const mediaTypesExists = hasVideoMediaType(bidRequest) || hasBannerMediaType(bidRequest); - if (!mediaTypesExists) { - return false; - } - - return true; -} - -/** - * Validates banner bid request. If it is not banner media type returns true. - * @param {object} bid, bid to validate - * @return boolean, true if valid, otherwise false - */ -function _validateBanner(bidRequest) { - // If there's no banner no need to validate - if (!hasBannerMediaType(bidRequest)) { - return true; - } - const banner = deepAccess(bidRequest, 'mediaTypes.banner'); - if (!Array.isArray(banner.sizes)) { - return false; - } - - return true; -} - -/** - * Validates video bid request. If it is not video media type returns true. - * @param {object} bid, bid to validate - * @return boolean, true if valid, otherwise false - */ -function _validateVideo(bidRequest) { - // If there's no video no need to validate - if (!hasVideoMediaType(bidRequest)) { - return true; - } - - const videoPlacement = deepAccess(bidRequest, 'mediaTypes.video', {}); - const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); - const params = deepAccess(bidRequest, 'params', {}); - - if (params && params.e2etest) { - return true; - } - - const videoParams = { - ...videoPlacement, - ...videoBidderParams // Bidder Specific overrides - }; - - if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { - logError('Validation failed: mimes are invalid'); - return false; - } - - if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { - logError('Validation failed: protocols are invalid'); - return false; - } - - if (!videoParams.context) { - logError('Validation failed: context id not declared'); - return false; - } - - if (videoParams.context !== 'instream') { - logError('Validation failed: only context instream is supported '); - return false; - } - - if (typeof videoParams.playerSize === 'undefined' || !Array.isArray(videoParams.playerSize) || !Array.isArray(videoParams.playerSize[0])) { - logError('Validation failed: player size not declared or is not in format [[w,h]]'); - return false; - } - - return true; -} - -/** - * Prepares video request data. - * - * @param bidRequest - * @param bidderRequest - * @returns openrtbRequest - */ -function buildVideoRequestData(bidRequest, bidderRequest) { - const {params} = bidRequest; - - const videoAdUnit = deepAccess(bidRequest, 'mediaTypes.video', {}); - const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); - - const videoParams = { - ...videoAdUnit, - ...videoBidderParams // Bidder Specific overrides - }; - - if (bidRequest.params && bidRequest.params.e2etest) { - videoParams.playerSize = [[640, 480]] - videoParams.conext = 'instream' - } - - const video = { - w: parseInt(videoParams.playerSize[0][0], 10), - h: parseInt(videoParams.playerSize[0][1], 10), - } - - // Obtain all ORTB params related video from Ad Unit - OPENRTB_VIDEO_PARAMS.forEach((param) => { - if (videoParams.hasOwnProperty(param)) { - video[param] = videoParams[param]; - } - }); - - // Placement Inference Rules: - // - If no placement is defined then default to 1 (In Stream) - video.placement = video.placement || 2; - - // - If product is instream (for instream context) then override placement to 1 - if (params.context === 'instream') { - video.startdelay = video.startdelay || 0; - video.placement = 1; - } - - // bid floor - const bidFloorRequest = { - currency: bidRequest.params.cur || 'USD', - mediaType: 'video', - size: '*' - }; - let floorData = bidRequest.params - if (isFn(bidRequest.getFloor)) { - floorData = bidRequest.getFloor(bidFloorRequest); - } else { - if (params.bidfloor) { - floorData = {floor: params.bidfloor, currency: params.currency || 'USD'}; - } - } - - const openrtbRequest = { - id: bidRequest.bidId, - imp: [ - { - id: '1', - video: video, - secure: isSecure() ? 1 : 0, - bidfloor: floorData.floor, - bidfloorcur: floorData.currency - } - ], - site: { - domain: bidderRequest.refererInfo.domain, - page: bidderRequest.refererInfo.page, - ref: bidderRequest.refererInfo.ref, - }, - ext: { - hb: 1, - prebidver: '$prebid.version$', - adapterver: spec.VERSION, - }, - }; - - // content - if (videoParams.content && isPlainObject(videoParams.content)) { - openrtbRequest.site.content = {}; - const contentStringKeys = ['id', 'title', 'series', 'season', 'genre', 'contentrating', 'language', 'url']; - const contentNumberkeys = ['episode', 'prodq', 'context', 'livestream', 'len']; - const contentArrayKeys = ['cat']; - const contentObjectKeys = ['ext']; - for (const contentKey in videoBidderParams.content) { - if ( - (contentStringKeys.indexOf(contentKey) > -1 && isStr(videoParams.content[contentKey])) || - (contentNumberkeys.indexOf(contentKey) > -1 && isNumber(videoParams.content[contentKey])) || - (contentObjectKeys.indexOf(contentKey) > -1 && isPlainObject(videoParams.content[contentKey])) || - (contentArrayKeys.indexOf(contentKey) > -1 && isArray(videoParams.content[contentKey]) && - videoParams.content[contentKey].every(catStr => isStr(catStr)))) { - openrtbRequest.site.content[contentKey] = videoParams.content[contentKey]; - } else { - logMessage('KultureMedia bid adapter validation error: ', contentKey, ' is either not supported is OpenRTB V2.5 or value is undefined'); - } - } - } - - return openrtbRequest; -} - -/** - * Prepares video request data. - * - * @param bidRequest - * @param bidderRequest - * @returns openrtbRequest - */ -function buildBannerRequestData(bidRequests, bidderRequest) { - const impr = bidRequests.map(bidRequest => ({ - id: bidRequest.bidId, - banner: { - format: bidRequest.mediaTypes.banner.sizes.map(sizeArr => ({ - w: sizeArr[0], - h: sizeArr[1] - })) - }, - ext: { - exchange: { - placementId: bidRequest.params.placementId - } - } - })); - - const openrtbRequest = { - id: bidderRequest.bidderRequestId, - imp: impr, - site: { - domain: bidderRequest.refererInfo?.domain, - page: bidderRequest.refererInfo?.page, - ref: bidderRequest.refererInfo?.ref, - }, - ext: {} - }; - return openrtbRequest; -} - -function _createBidResponse(bid) { - const isADomainPresent = - bid.adomain && bid.adomain.length; - const bidResponse = { - requestId: bid.impid, - cpm: bid.price, - width: bid.w, - height: bid.h, - ad: bid.adm, - ttl: typeof bid.exp === 'number' ? bid.exp : DEFAULT_BID_TTL, - creativeId: bid.crid, - netRevenue: DEFAULT_NET_REVENUE, - currency: DEFAULT_CURRENCY, - mediaType: deepAccess(bid, 'ext.prebid.type', BANNER) - } - - if (isADomainPresent) { - bidResponse.meta = { - advertiserDomains: bid.adomain - }; - } - - if (bidResponse.mediaType === VIDEO) { - bidResponse.vastXml = bid.adm; - } - - return bidResponse; -} - -function isSecure() { - return document.location.protocol === 'https:'; -} - -registerBidder(spec); diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 3157a4c155e..cc43f6f6f84 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -8,12 +8,15 @@ import { triggerPixel, logError } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports -import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {UID2_EIDS} from '../libraries/uid2Eids/uid2Eids.js'; +const DEFAULT_AJAX_TIMEOUT = 5000 const EVENTS_TOPIC = 'pre_lips' const MODULE_NAME = 'liveIntentId'; +const LI_PROVIDER_DOMAIN = 'liveintent.com'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); const defaultRequestedAttributes = {'nonId': true} const calls = { @@ -64,6 +67,7 @@ function parseLiveIntentCollectorConfig(collectConfig) { collectConfig.fpiStorageStrategy && (config.storageStrategy = collectConfig.fpiStorageStrategy); collectConfig.fpiExpirationDays && (config.expirationDays = collectConfig.fpiExpirationDays); collectConfig.collectorUrl && (config.collectorUrl = collectConfig.collectorUrl); + config.ajaxTimeout = collectConfig.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; return config; } @@ -98,9 +102,8 @@ function initializeLiveConnect(configParams) { if (configParams.url) { identityResolutionConfig.url = configParams.url } - if (configParams.ajaxTimeout) { - identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout; - } + + identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; const liveConnectConfig = parseLiveIntentCollectorConfig(configParams.liCollectConfig); @@ -124,7 +127,11 @@ function initializeLiveConnect(configParams) { liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; liveConnectConfig.gdprConsent = gdprConsent.consentString; } - + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + liveConnectConfig.gppString = gppConsent.gppString; + liveConnectConfig.gppApplicableSections = gppConsent.applicableSections; + } // The second param is the storage object, LS & Cookie manipulation uses PBJS // The third param is the ajax and pixel object, the ajax and pixel use PBJS liveConnect = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); @@ -189,15 +196,31 @@ export const liveIntentIdSubmodule = { // As adapters are applied in lexicographical order, we will always // be overwritten by the 'proper' uid2 module if it is present. if (value.uid2) { - result.uid2 = { 'id': value.uid2 } + result.uid2 = { 'id': value.uid2, ext: { provider: LI_PROVIDER_DOMAIN } } } if (value.bidswitch) { - result.bidswitch = { 'id': value.bidswitch } + result.bidswitch = { 'id': value.bidswitch, ext: { provider: LI_PROVIDER_DOMAIN } } } if (value.medianet) { - result.medianet = { 'id': value.medianet } + result.medianet = { 'id': value.medianet, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.magnite) { + result.magnite = { 'id': value.magnite, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.index) { + result.index = { 'id': value.index, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.openx) { + result.openx = { 'id': value.openx, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.pubmatic) { + result.pubmatic = { 'id': value.pubmatic, ext: { provider: LI_PROVIDER_DOMAIN } } } return result @@ -237,6 +260,95 @@ export const liveIntentIdSubmodule = { } return { callback: result }; + }, + eids: { + ...UID2_EIDS, + 'lipb': { + getValue: function(data) { + return data.lipbid; + }, + source: 'liveintent.com', + atype: 3, + getEidExt: function(data) { + if (Array.isArray(data.segments) && data.segments.length) { + return { + segments: data.segments + }; + } + } + }, + 'bidswitch': { + source: 'bidswitch.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'medianet': { + source: 'media.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'magnite': { + source: 'rubiconproject.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'index': { + source: 'liveintent.indexexchange.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'openx': { + source: 'openx.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'pubmatic': { + source: 'pubmatic.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + } } }; diff --git a/modules/livewrappedAnalyticsAdapter.js b/modules/livewrappedAnalyticsAdapter.js index fe69220e123..f3ee81cae7a 100644 --- a/modules/livewrappedAnalyticsAdapter.js +++ b/modules/livewrappedAnalyticsAdapter.js @@ -168,7 +168,7 @@ livewrappedAnalyticsAdapter.sendEvents = function() { requests: sentRequests.sentRequests, responses: getResponses(sentRequests.gdpr, sentRequests.auctionIds), wins: getWins(sentRequests.gdpr, sentRequests.auctionIds), - timeouts: getTimeouts(sentRequests.auctionIds), + timeouts: getTimeouts(sentRequests.gdpr, sentRequests.auctionIds), bidAdUnits: getbidAdUnits(), rf: getAdRenderFailed(sentRequests.auctionIds), rcv: getAdblockerRecovered() @@ -237,27 +237,9 @@ function getResponses(gdpr, auctionIds) { if (bid.readyToSend && !(bid.sendStatus & RESPONSESENT) && !bid.timeout) { bid.sendStatus |= RESPONSESENT; - responses.push({ - timeStamp: auction.timeStamp, - adUnit: bid.adUnit, - adUnitId: bid.adUnitId, - bidder: bid.bidder, - width: bid.width, - height: bid.height, - cpm: bid.cpm, - orgCpm: bid.originalCpm, - ttr: bid.ttr, - IsBid: bid.isBid, - mediaType: bid.mediaType, - gdpr: gdprPos, - floor: bid.lwFloor ? bid.lwFloor : (bid.floorData ? bid.floorData.floorValue : undefined), - floorCur: bid.floorData ? bid.floorData.floorCurrency : undefined, - auctionId: auctionIdPos, - auc: bid.auc, - buc: bid.buc, - lw: bid.lw, - meta: bid.meta - }); + let response = getResponseObject(auction, bid, gdprPos, auctionIdPos); + + responses.push(response); } }); }); @@ -337,27 +319,45 @@ function getAuctionIdPos(auctionIds, auctionId) { return auctionIdPos; } -function getTimeouts(auctionIds) { +function getResponseObject(auction, bid, gdprPos, auctionIdPos) { + return { + timeStamp: auction.timeStamp, + adUnit: bid.adUnit, + adUnitId: bid.adUnitId, + bidder: bid.bidder, + width: bid.width, + height: bid.height, + cpm: bid.cpm, + orgCpm: bid.originalCpm, + ttr: bid.ttr, + IsBid: bid.isBid, + mediaType: bid.mediaType, + gdpr: gdprPos, + floor: bid.lwFloor ? bid.lwFloor : (bid.floorData ? bid.floorData.floorValue : undefined), + floorCur: bid.floorData ? bid.floorData.floorCurrency : undefined, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc, + lw: bid.lw, + meta: bid.meta + }; +} + +function getTimeouts(gdpr, auctionIds) { var timeouts = []; Object.keys(cache.auctions).forEach(auctionId => { let auctionIdPos = getAuctionIdPos(auctionIds, auctionId); Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { let auction = cache.auctions[auctionId]; + let gdprPos = getGdprPos(gdpr, auction); let bid = auction.bids[bidId]; if (!(bid.sendStatus & TIMEOUTSENT) && bid.timeout) { bid.sendStatus |= TIMEOUTSENT; - timeouts.push({ - bidder: bid.bidder, - adUnit: bid.adUnit, - adUnitId: bid.adUnitId, - timeStamp: auction.timeStamp, - auctionId: auctionIdPos, - auc: bid.auc, - buc: bid.buc, - lw: bid.lw - }); + let timeout = getResponseObject(auction, bid, gdprPos, auctionIdPos); + + timeouts.push(timeout); } }); }); diff --git a/modules/lm_kiviadsBidAdapter.js b/modules/lm_kiviadsBidAdapter.js new file mode 100644 index 00000000000..9ba26052727 --- /dev/null +++ b/modules/lm_kiviadsBidAdapter.js @@ -0,0 +1,207 @@ +import {config} from '../src/config.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +const CUR = 'USD'; +const BIDDER_CODE = 'lm_kiviads'; +const ENDPOINT = 'https://pbjs.kiviads.live'; + +/** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ +function isBidRequestValid(req) { + if (req && typeof req.params !== 'object') { + logError('Params is not defined or is incorrect in the bidder settings'); + return false; + } + + if (!getBidIdParameter('env', req.params) || !getBidIdParameter('pid', req.params)) { + logError('Env or pid is not present in bidder params'); + return false; + } + + if (deepAccess(req, 'mediaTypes.video') && !isArray(deepAccess(req, 'mediaTypes.video.playerSize'))) { + logError('mediaTypes.video.playerSize is required for video'); + return false; + } + + return true; +} + +/** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequest?pbjs_debug=trues[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ +function buildRequests(validBidRequests, bidderRequest) { + const {refererInfo = {}, gdprConsent = {}, uspConsent} = bidderRequest; + const requests = validBidRequests.map(req => { + const request = {}; + request.bidId = req.bidId; + request.banner = deepAccess(req, 'mediaTypes.banner'); + request.auctionId = req.ortb2?.source?.tid; + request.transactionId = req.ortb2Imp?.ext?.tid; + request.sizes = parseSizesInput(getAdUnitSizes(req)); + request.schain = req.schain; + request.location = { + page: refererInfo.page, + location: refererInfo.location, + domain: refererInfo.domain, + whost: window.location.host, + ref: refererInfo.ref, + isAmp: refererInfo.isAmp + }; + request.device = { + ua: navigator.userAgent, + lang: navigator.language + }; + request.env = { + env: req.params.env, + pid: req.params.pid + }; + request.ortb2 = req.ortb2; + request.ortb2Imp = req.ortb2Imp; + request.tz = new Date().getTimezoneOffset(); + request.ext = req.params.ext; + request.bc = req.bidRequestsCount; + request.floor = getBidFloor(req); + + if (req.userIdAsEids && req.userIdAsEids.length !== 0) { + request.userEids = req.userIdAsEids; + } else { + request.userEids = []; + } + if (gdprConsent.gdprApplies) { + request.gdprApplies = Number(gdprConsent.gdprApplies); + request.consentString = gdprConsent.consentString; + } else { + request.gdprApplies = 0; + request.consentString = ''; + } + if (uspConsent) { + request.usPrivacy = uspConsent; + } else { + request.usPrivacy = ''; + } + if (config.getConfig('coppa')) { + request.coppa = 1; + } else { + request.coppa = 0; + } + + const video = deepAccess(req, 'mediaTypes.video'); + if (video) { + request.sizes = parseSizesInput(deepAccess(req, 'mediaTypes.video.playerSize')); + request.video = video; + } + + return request; + }); + + return { + method: 'POST', + url: ENDPOINT + '/bid', + data: JSON.stringify(requests), + withCredentials: true, + bidderRequest, + options: { + contentType: 'application/json', + } + }; +} + +/** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function interpretResponse(serverResponse, {bidderRequest}) { + const response = []; + if (!isArray(deepAccess(serverResponse, 'body.data'))) { + return response; + } + + serverResponse.body.data.forEach(serverBid => { + const bid = { + requestId: bidderRequest.bidId, + dealId: bidderRequest.dealId || null, + ...serverBid + }; + response.push(bid); + }); + + return response; +} + +/** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ +function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + const syncs = []; + const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); + + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && isArray(pixels) && pixels.length !== 0) { + const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; + const usPrivacy = `us_privacy=${encodeURIComponent(uspConsent)}`; + + pixels.forEach(pixel => { + const [type, url] = pixel; + const sync = {type, url: `${url}&${usPrivacy}${gdprFlag}${gdprString}`}; + if (type === 'iframe' && syncOptions.iframeEnabled) { + syncs.push(sync) + } else if (type === 'image' && syncOptions.pixelEnabled) { + syncs.push(sync) + } + }); + } + + return syncs; +} + +/** + * Get valid floor value from getFloor fuction. + * + * @param {Object} bid Current bid request. + * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return null; + } + + let floor = bid.getFloor({ + currency: CUR, + mediaType: '*', + size: '*' + }); + + if (typeof floor === 'object' && !isNaN(floor.floor) && floor.currency === CUR) { + return floor.floor; + } + + return null; +} + +export const spec = { + code: BIDDER_CODE, + aliases: ['kivi'], + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +} + +registerBidder(spec); diff --git a/modules/lm_kiviadsBidAdapter.md b/modules/lm_kiviadsBidAdapter.md new file mode 100644 index 00000000000..fc1b05d1ef7 --- /dev/null +++ b/modules/lm_kiviadsBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + +``` +Module Name: lm_kiviads Bidder Adapter +Module Type: lm_kiviads Bidder Adapter +Maintainer: pavlo@xe.works +``` + +# Description + +Module that connects to kiviads.com demand sources + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + } + } + ] + }, + { + code: 'test-video', + sizes: [ [ 640, 480 ] ], + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } + }, + bids: [{ + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + } + }] + } +]; +``` diff --git a/modules/lockerdomeBidAdapter.js b/modules/lockerdomeBidAdapter.js index 5c38753c1e2..5038eadce30 100644 --- a/modules/lockerdomeBidAdapter.js +++ b/modules/lockerdomeBidAdapter.js @@ -1,6 +1,6 @@ -import { getBidIdParameter } from '../src/utils.js'; import {BANNER} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getBidIdParameter} from '../src/utils.js'; export const spec = { code: 'lockerdome', diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index 02b01b8bd9d..808a67492b0 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -368,6 +368,12 @@ export const lotamePanoramaIdSubmodule = { return { callback: resolveIdFunction }; }, + eids: { + lotamePanoramaId: { + source: 'crwdcntrl.net', + atype: 1, + }, + }, }; submodule('userId', lotamePanoramaIdSubmodule); diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index b501e3bef5a..b9665b93494 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -142,6 +142,10 @@ const sendEvent = payload => { ...getTopLevelDetails(), ...payload } + if (window.pbjs?.rp?.eventDispatcher) { + const analyticsEvent = new CustomEvent('beforeSendingMagniteAnalytics', { detail: event }); + window.pbjs.rp.eventDispatcher.dispatchEvent(analyticsEvent); + } ajax( endpoint, null, diff --git a/modules/mediabramaBidAdapter.js b/modules/mediabramaBidAdapter.js new file mode 100644 index 00000000000..caf6854fe03 --- /dev/null +++ b/modules/mediabramaBidAdapter.js @@ -0,0 +1,155 @@ +import { + isFn, + isStr, + deepAccess, + getWindowTop, + triggerPixel +} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'mediabrama'; +const AD_URL = 'https://prebid.mediabrama.com/pbjs'; +const SYNC_URL = 'https://prebid.mediabrama.com/sync'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency || !bid.meta) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + default: + return false; + } +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidFloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0 + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && bid.params.placementId); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + const winTop = getWindowTop(); + const location = winTop.location; + const placements = []; + + const request = { + deviceWidth: winTop.screen.width, + deviceHeight: winTop.screen.height, + language: (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + host: location.host, + page: location.pathname, + placements: placements + }; + + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent; + } + } + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const placement = { + placementId: bid.params.placementId, + bidId: bid.bidId, + schain: bid.schain || {}, + bidfloor: getBidFloor(bid) + }; + + if (typeof bid.userId !== 'undefined') { + placement.userId = bid.userId; + } + + const mediaType = bid.mediaTypes; + + if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { + placement.sizes = mediaType[BANNER].sizes; + placement.adFormat = BANNER; + } + + placements.push(placement); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + }, + + onBidWon: (bid) => { + const cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || ''; + if (isStr(bid.nurl) && bid.nurl !== '') { + bid.nurl = bid.nurl.replace(/\${AUCTION_PRICE}/, cpm); + triggerPixel(bid.nurl); + } + } +}; + +registerBidder(spec); diff --git a/modules/mediabramaBidAdapter.md b/modules/mediabramaBidAdapter.md new file mode 100644 index 00000000000..fde0a399852 --- /dev/null +++ b/modules/mediabramaBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: MediaBrama Bidder Adapter +Module Type: MediaBrama Bidder Adapter +Maintainer: support@mediabrama.com +``` + +# Description + +Module that connects to mediabrama demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'div-prebid', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'mediabrama', + params: { + placementId: '24428' //test, please replace after test + } + } + ] + }, + ]; +``` diff --git a/modules/mediafilterRtdProvider.js b/modules/mediafilterRtdProvider.js new file mode 100644 index 00000000000..277b19c10b2 --- /dev/null +++ b/modules/mediafilterRtdProvider.js @@ -0,0 +1,94 @@ +/** + * This module adds the Media Filter real-time ad monitoring and protection module. + * + * The {@link module:modules/realTimeData} module is required + * + * For more information, visit {@link https://www.themediatrust.com The Media Trust}. + * + * @author Mirnes Cajlakovic + * @module modules/mediafilterRtdProvider + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js'; +import { logError, generateUUID } from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; + +/** @const {string} MEDIAFILTER_EVENT_TYPE - The event type for Media Filter. */ +export const MEDIAFILTER_EVENT_TYPE = 'com.mediatrust.pbjs.'; +/** @const {string} MEDIAFILTER_BASE_URL - The base URL for Media Filter scripts. */ +export const MEDIAFILTER_BASE_URL = 'https://scripts.webcontentassessor.com/scripts/'; + +export const MediaFilter = { + /** + * Registers the Media Filter as a submodule of real-time data. + */ + register: function() { + submodule('realTimeData', { + 'name': 'mediafilter', + 'init': this.generateInitHandler() + }); + }, + + /** + * Sets up the Media Filter by initializing event listeners and loading the external script. + * @param {object} configuration - The configuration object. + */ + setup: function(configuration) { + this.setupEventListener(configuration.configurationHash); + this.setupScript(configuration.configurationHash); + }, + + /** + * Sets up an event listener for Media Filter messages. + * @param {string} configurationHash - The configuration hash. + */ + setupEventListener: function(configurationHash) { + window.addEventListener('message', this.generateEventHandler(configurationHash)); + }, + + /** + * Loads the Media Filter script based on the provided configuration hash. + * @param {string} configurationHash - The configuration hash. + */ + setupScript: function(configurationHash) { + loadExternalScript(MEDIAFILTER_BASE_URL.concat(configurationHash), 'mediafilter', () => {}); + }, + + /** + * Generates an event handler for Media Filter messages. + * @param {string} configurationHash - The configuration hash. + * @returns {function} The generated event handler. + */ + generateEventHandler: function(configurationHash) { + return (windowEvent) => { + if (windowEvent.data.type === MEDIAFILTER_EVENT_TYPE.concat('.', configurationHash)) { + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + 'billingId': generateUUID(), + 'configurationHash': configurationHash, + 'type': 'impression', + 'vendor': 'mediafilter', + }); + } + }; + }, + + /** + * Generates an initialization handler for Media Filter. + * @returns {function} The generated init handler. + */ + generateInitHandler: function() { + return (configuration) => { + try { + this.setup(configuration); + } catch (error) { + logError(`Error in initialization: ${error.message}`); + } + }; + } +}; + +// Register the module +MediaFilter.register(); diff --git a/modules/mediafilterRtdProvider.md b/modules/mediafilterRtdProvider.md new file mode 100644 index 00000000000..469479f8d0b --- /dev/null +++ b/modules/mediafilterRtdProvider.md @@ -0,0 +1,37 @@ +## Overview + +**Module:** The Media Filter +**Type: **Real Time Data Module + +As malvertising, scams, and controversial and offensive ad content proliferate across the digital media ecosystem, publishers need advanced controls to both shield audiences from malware attacks and ensure quality site experience. With the market’s fastest and most comprehensive real-time ad quality tool, The Media Trust empowers publisher Ad/Revenue Operations teams to block a wide range of malware, high-risk ad platforms, heavy ads, ads with sensitive or objectionable content, and custom lists (e.g., competitors). Customizable replacement code calls for a new ad to ensure impressions are still monetized. + +[![IMAGE ALT TEXT](http://img.youtube.com/vi/VBHRiirge7s/0.jpg)](http://www.youtube.com/watch?v=VBHRiirge7s "Publishers' Ultimate Avenger: Media Filter") + +To start using this module, please contact [The Media Trust](https://mediatrust.com/how-we-help/media-filter/ "The Media Trust") to get a script and configuration hash for module configuration. + +## Integration + +1. Build Prebid bundle with The Media Filter module included. + +``` +gulp build --modules=mediafilterRtdProvider +``` + +2. Inlcude the bundled script in your application. + +## Configuration + +Add configuration entry to `realTimeData.dataProviders` for The Media Filter module. + +``` +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'mediafilter', + params: { + configurationHash: '', + } + }] + } +}); +``` diff --git a/modules/mediafuseBidAdapter.js b/modules/mediafuseBidAdapter.js index 98179c49e0d..1fdd3530fae 100644 --- a/modules/mediafuseBidAdapter.js +++ b/modules/mediafuseBidAdapter.js @@ -1,14 +1,8 @@ import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, isArray, isArrayOfNums, @@ -38,7 +32,10 @@ import { getANKewyordParamFromMaps, getANKeywordParam, transformBidderParamKeywords -} from '../libraries/appnexusKeywords/anKeywords.js'; +} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const BIDDER_CODE = 'mediafuse'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -959,7 +956,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -985,7 +982,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js index 055fef3bb39..efd730acace 100644 --- a/modules/mediagoBidAdapter.js +++ b/modules/mediagoBidAdapter.js @@ -221,10 +221,10 @@ function getItems(validBidRequests, bidderRequest) { } const bidFloor = getBidFloor(req); - // const gpid = - // utils.deepAccess(req, 'ortb2Imp.ext.gpid') || - // utils.deepAccess(req, 'ortb2Imp.ext.data.pbadslot') || - // utils.deepAccess(req, 'params.placementId', 0); + const gpid = + utils.deepAccess(req, 'ortb2Imp.ext.gpid') || + utils.deepAccess(req, 'ortb2Imp.ext.data.pbadslot') || + utils.deepAccess(req, 'params.placementId', 0); // if (mediaTypes.native) {} // banner广告类型 @@ -240,8 +240,10 @@ function getItems(validBidRequests, bidderRequest) { format: sizes, }, ext: { - // gpid: gpid, // 加入后无法返回广告 + ortb2Imp: utils.deepAccess(req, 'ortb2Imp'), // 传入完整对象,分析日志数据 + gpid: gpid, // 加入后无法返回广告 }, + tagid: req.params && req.params.tagid, }; itemMaps[id] = { req, diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index b9e00f45df9..041db71cd34 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -1,7 +1,6 @@ import { buildUrl, deepAccess, - getGptSlotInfoForAdUnitCode, getWindowTop, isArray, isEmpty, @@ -18,9 +17,12 @@ import {getRefererInfo} from '../src/refererDetection.js'; import {Renderer} from '../src/Renderer.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'medianet'; +const TRUSTEDSTACK_CODE = 'trustedstack'; const BID_URL = 'https://prebid.media.net/rtb/prebid'; +const TRUSTEDSTACK_URL = 'https://prebid.trustedstack.com/rtb/trustedstack'; const PLAYER_URL = 'https://prebid.media.net/video/bundle.js'; const SLOT_VISIBILITY = { NOT_DETERMINED: 0, @@ -49,7 +51,7 @@ mnData.urlData = { }; const aliases = [ - { code: 'aax', gvlid: 720 }, + { code: TRUSTEDSTACK_CODE }, ]; getGlobal().medianetGlobals = getGlobal().medianetGlobals || {}; @@ -323,8 +325,9 @@ function normalizeCoordinates(coordinates) { } } -function getBidderURL(cid) { - return BID_URL + '?cid=' + encodeURIComponent(cid); +function getBidderURL(bidderCode, cid) { + const url = (bidderCode === TRUSTEDSTACK_CODE) ? TRUSTEDSTACK_URL : BID_URL; + return url + '?cid=' + encodeURIComponent(cid); } function generatePayload(bidRequests, bidderRequests) { @@ -463,7 +466,7 @@ export const spec = { let payload = generatePayload(bidRequests, bidderRequests); return { method: 'POST', - url: getBidderURL(payload.ext.customer_id), + url: getBidderURL(bidderRequests.bidderCode, payload.ext.customer_id), data: JSON.stringify(payload) }; }, diff --git a/modules/mediasniperBidAdapter.js b/modules/mediasniperBidAdapter.js index 378a804487a..aee5f6230b2 100644 --- a/modules/mediasniperBidAdapter.js +++ b/modules/mediasniperBidAdapter.js @@ -1,8 +1,7 @@ import { deepAccess, deepClone, - deepSetValue, - getBidIdParameter, + deepSetValue, getBidIdParameter, inIframe, isArray, isEmpty, diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 720063253de..1db4dad84cc 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -120,23 +120,17 @@ export const spec = { netRevenue: value['net_revenue'], ttl: value['ttl'], ad: value['ad'], - mediasquare: { - 'bidder': value['bidder'], - 'code': value['code'] - }, + mediasquare: {}, meta: { 'advertiserDomains': value['adomain'] } }; - if ('context' in value) { - bidResponse['mediasquare']['context'] = value['context']; - } - if ('match' in value) { - bidResponse['mediasquare']['match'] = value['match']; - } - if ('hasConsent' in value) { - bidResponse['mediasquare']['hasConsent'] = value['hasConsent']; - } + let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment', 'ova']; + paramsToSearchFor.forEach(param => { + if (param in value) { + bidResponse['mediasquare'][param] = value[param]; + } + }); if ('native' in value) { bidResponse['native'] = value['native']; bidResponse['mediaType'] = 'native'; @@ -180,21 +174,26 @@ export const spec = { } let params = { pbjs: '$prebid.version$', referer: encodeURIComponent(getRefererInfo().page || getRefererInfo().topmostLocation) }; let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD; - let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context']; + let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment', 'ova']; if (bid.hasOwnProperty('mediasquare')) { - for (let i = 0; i < paramsToSearchFor.length; i++) { - if (bid['mediasquare'].hasOwnProperty(paramsToSearchFor[i])) { - params[paramsToSearchFor[i]] = bid['mediasquare'][paramsToSearchFor[i]]; + paramsToSearchFor.forEach(param => { + if (bid['mediasquare'].hasOwnProperty(param)) { + params[param] = bid['mediasquare'][param]; + if (typeof params[param] == 'number') { + params[param] = params[param].toString(); + } } - } + }); }; paramsToSearchFor = ['cpm', 'size', 'mediaType', 'currency', 'creativeId', 'adUnitCode', 'timeToRespond', 'requestId', 'auctionId', 'originalCpm', 'originalCurrency']; - for (let i = 0; i < paramsToSearchFor.length; i++) { - if (bid.hasOwnProperty(paramsToSearchFor[i])) { - params[paramsToSearchFor[i]] = bid[paramsToSearchFor[i]]; - if (typeof params[paramsToSearchFor[i]] == 'number') { params[paramsToSearchFor[i]] = params[paramsToSearchFor[i]].toString() } + paramsToSearchFor.forEach(param => { + if (bid.hasOwnProperty(param)) { + params[param] = bid[param]; + if (typeof params[param] == 'number') { + params[param] = params[param].toString(); + } } - } + }); ajax(endpoint + BIDDER_ENDPOINT_WINNING, null, JSON.stringify(params), {method: 'POST', withCredentials: true}); return true; } diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js index c522d588970..fc77c7cc97d 100644 --- a/modules/merkleIdSystem.js +++ b/modules/merkleIdSystem.js @@ -196,6 +196,30 @@ export const merkleIdSubmodule = { logInfo('User ID - merkleId not refreshed'); return {id: storedId}; + }, + eids: { + 'merkleId': { + atype: 3, + getSource: function(data) { + if (data?.ext?.ssp) { + return `${data.ext.ssp}.merkleinc.com` + } + return 'merkleinc.com' + }, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.keyID) { + return { + keyID: data.keyID + } + } + if (data.ext) { + return data.ext; + } + } + }, } }; diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 8e889261e52..1e158236deb 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -9,11 +9,10 @@ import { isEmpty, triggerPixel, logWarn, - getBidIdParameter, isFn, isNumber, isBoolean, - isInteger, deepSetValue, + isInteger, deepSetValue, getBidIdParameter, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; diff --git a/modules/minutemediaBidAdapter.js b/modules/minutemediaBidAdapter.js index 85c1386957d..e67534d74fe 100644 --- a/modules/minutemediaBidAdapter.js +++ b/modules/minutemediaBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -307,6 +319,15 @@ function generateBidParameters(bid, bidderRequest) { bidObject.placementId = placementId; } + const mimes = deepAccess(bid, `mediaTypes.${mediaType}.mimes`); + if (mimes) { + bidObject.mimes = mimes; + } + const api = deepAccess(bid, `mediaTypes.${mediaType}.api`); + if (api) { + bidObject.api = api; + } + const sua = deepAccess(bid, `ortb2.device.sua`); if (sua) { bidObject.sua = sua; @@ -358,6 +379,11 @@ function generateBidParameters(bid, bidderRequest) { bidObject.linearity = linearity; } + const protocols = deepAccess(bid, `mediaTypes.video.protocols`); + if (protocols) { + bidObject.protocols = protocols; + } + const plcmt = deepAccess(bid, `mediaTypes.video.plcmt`); if (plcmt) { bidObject.plcmt = plcmt; @@ -398,7 +424,8 @@ function generateGeneralParams(generalObject, bidderRequest) { dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, device_type: getDeviceType(navigator.userAgent), ua: navigator.userAgent, - session_id: getBidIdParameter('bidderRequestId', generalObject), + is_wrapper: !!generalBidParams.isWrapper, + session_id: generalBidParams.sessionId || getBidIdParameter('bidderRequestId', generalObject), tmax: timeout } @@ -441,7 +468,7 @@ function generateGeneralParams(generalObject, bidderRequest) { if (bidderRequest && bidderRequest.refererInfo) { generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.ref'); - generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || window.location.href + generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); } return generalParams diff --git a/modules/minutemediaplusBidAdapter.js b/modules/minutemediaplusBidAdapter.js index 33f3634e17f..146d437b1fa 100644 --- a/modules/minutemediaplusBidAdapter.js +++ b/modules/minutemediaplusBidAdapter.js @@ -252,13 +252,20 @@ function interpretResponse(serverResponse, request) { } } -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { let syncs = []; const {iframeEnabled, pixelEnabled} = syncOptions; const {gdprApplies, consentString = ''} = gdprConsent; + const {gppString, applicableSections} = gppConsent; const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + + if (gppString && applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppString); + params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); + } + if (iframeEnabled) { syncs.push({ type: 'iframe', diff --git a/modules/missenaBidAdapter.js b/modules/missenaBidAdapter.js index 33fa6857e85..1ab6d0c6945 100644 --- a/modules/missenaBidAdapter.js +++ b/modules/missenaBidAdapter.js @@ -1,4 +1,11 @@ -import { buildUrl, formatQS, logInfo, triggerPixel } from '../src/utils.js'; +import { + buildUrl, + formatQS, + isFn, + logInfo, + triggerPixel, +} from '../src/utils.js'; +import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -7,6 +14,22 @@ const ENDPOINT_URL = 'https://bid.missena.io/'; const EVENTS_DOMAIN = 'events.missena.io'; const EVENTS_DOMAIN_DEV = 'events.staging.missena.xyz'; +/* Get Floor price information */ +function getFloor(bidRequest) { + if (!isFn(bidRequest.getFloor)) { + return {}; + } + + const bidFloors = bidRequest.getFloor({ + currency: 'USD', + mediaType: BANNER + }); + + if (!isNaN(bidFloors.floor)) { + return bidFloors; + } +} + export const spec = { aliases: ['msna'], code: BIDDER_CODE, @@ -61,6 +84,12 @@ export const spec = { payload.is_internal = bidRequest.params.isInternal; } payload.userEids = bidRequest.userIdAsEids || []; + + const bidFloor = getFloor(bidRequest); + payload.floor = bidFloor?.floor; + payload.floor_currency = bidFloor?.currency; + payload.currency = config.getConfig('currency.adServerCurrency') || 'EUR'; + return { method: 'POST', url: baseUrl + '?' + formatQS({ t: bidRequest.params.apiKey }), @@ -89,7 +118,7 @@ export const spec = { syncOptions, serverResponses, gdprConsent, - uspConsent + uspConsent, ) { if (!syncOptions.iframeEnabled) { return []; @@ -128,8 +157,13 @@ export const spec = { protocol: 'https', hostname, pathname: '/v1/bidsuccess', - search: { t: bid.params[0].apiKey, provider: bid.meta?.networkName, cpm: bid.cpm, currency: bid.currency }, - }) + search: { + t: bid.params[0].apiKey, + provider: bid.meta?.networkName, + cpm: bid.originalCpm, + currency: bid.originalCurrency, + }, + }), ); logInfo('Missena - Bid won', bid); }, diff --git a/modules/mobfoxpbBidAdapter.js b/modules/mobfoxpbBidAdapter.js index 9ff50e2531f..35e9b03c031 100644 --- a/modules/mobfoxpbBidAdapter.js +++ b/modules/mobfoxpbBidAdapter.js @@ -71,6 +71,15 @@ export const spec = { if (bidderRequest.gdprConsent) { request.gdpr = bidderRequest.gdprConsent; } + + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } } const len = validBidRequests.length; diff --git a/modules/mwOpenLinkIdSystem.js b/modules/mwOpenLinkIdSystem.js index 9b1035cbf18..ff23547224b 100644 --- a/modules/mwOpenLinkIdSystem.js +++ b/modules/mwOpenLinkIdSystem.js @@ -137,6 +137,12 @@ export const mwOpenLinkIdSubModule = { const submoduleConfigParams = (submoduleConfig && submoduleConfig.params) || {}; if (!isValidConfig(submoduleConfigParams)) return undefined; return setID(submoduleConfigParams); + }, + eids: { + 'mwOpenLinkId': { + source: 'mediawallahscript.com', + atype: 1 + }, } }; diff --git a/modules/nativoBidAdapter.js b/modules/nativoBidAdapter.js index c62a74e6d6c..69a270247cd 100644 --- a/modules/nativoBidAdapter.js +++ b/modules/nativoBidAdapter.js @@ -2,7 +2,20 @@ import { deepAccess, isEmpty } from '../src/utils.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER } from '../src/mediaTypes.js' import { getGlobal } from '../src/prebidGlobal.js' -// import { config } from 'src/config' +import { ortbConverter } from '../libraries/ortbConverter/converter.js' + +const converter = ortbConverter({ + context: { + // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: 30 // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + imp.tagid = bidRequest.adUnitCode + return imp; + } +}); const BIDDER_CODE = 'nativo' const BIDDER_ENDPOINT = 'https://exchange.postrelease.com/prebid' @@ -136,6 +149,10 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + // Get OpenRTB Data + const openRTBData = converter.toORTB({bidRequests: validBidRequests, bidderRequest}) + const openRTBDataString = JSON.stringify(openRTBData) + const requestData = new RequestData() requestData.addBidRequestDataSource(new UserEIDs()) @@ -271,8 +288,9 @@ export const spec = { const requestUrl = buildRequestUrl(BIDDER_ENDPOINT, qsParamStrings) let serverRequest = { - method: 'GET', - url: requestUrl + method: 'POST', + url: requestUrl, + data: openRTBDataString, } return serverRequest diff --git a/modules/naveggIdSystem.js b/modules/naveggIdSystem.js index 878ae7fadb2..8a472259873 100644 --- a/modules/naveggIdSystem.js +++ b/modules/naveggIdSystem.js @@ -107,6 +107,12 @@ export const naveggIdSubmodule = { } } return undefined; + }, + eids: { + 'naveggId': { + source: 'navegg.com', + atype: 1 + }, } }; submodule('userId', naveggIdSubmodule); diff --git a/modules/netIdSystem.js b/modules/netIdSystem.js index 90c8735c993..6f1ffe8b0e7 100644 --- a/modules/netIdSystem.js +++ b/modules/netIdSystem.js @@ -34,6 +34,12 @@ export const netIdSubmodule = { getId(config) { /* currently not possible */ return {}; + }, + eids: { + 'netId': { + source: 'netid.de', + atype: 1 + }, } }; diff --git a/modules/neuwoRtdProvider.js b/modules/neuwoRtdProvider.js index 00a3c59b4a6..881bbc10b11 100644 --- a/modules/neuwoRtdProvider.js +++ b/modules/neuwoRtdProvider.js @@ -10,14 +10,14 @@ const SEGTAX_IAB = 6 // IAB - Content Taxonomy version 2 const RESPONSE_IAB_TIER_1 = 'marketing_categories.iab_tier_1' const RESPONSE_IAB_TIER_2 = 'marketing_categories.iab_tier_2' -function init(config = {}, userConsent) { - config.params = config.params || {} +function init(config, userConsent) { + // config.params = config.params || {} // ignore module if publicToken is missing (module setup failure) - if (!config.params.publicToken) { + if (!config || !config.params || !config.params.publicToken) { logError('publicToken missing', 'NeuwoRTDModule', 'config.params.publicToken') return false; } - if (!config.params.apiUrl) { + if (!config || !config.params || !config.params.apiUrl) { logError('apiUrl missing', 'NeuwoRTDModule', 'config.params.apiUrl') return false; } @@ -25,14 +25,14 @@ function init(config = {}, userConsent) { } export function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - config.params = config.params || {}; + const confParams = config.params || {}; logInfo('NeuwoRTDModule', 'starting getBidRequestData') - const wrappedArgUrl = encodeURIComponent(config.params.argUrl || getRefererInfo().page); + const wrappedArgUrl = encodeURIComponent(confParams.argUrl || getRefererInfo().page); /* adjust for pages api.url?prefix=test (to add params with '&') as well as api.url (to add params with '?') */ - const joiner = config.params.apiUrl.indexOf('?') < 0 ? '?' : '&' - const url = config.params.apiUrl + joiner + [ - 'token=' + config.params.publicToken, + const joiner = confParams.apiUrl.indexOf('?') < 0 ? '?' : '&' + const url = confParams.apiUrl + joiner + [ + 'token=' + confParams.publicToken, 'url=' + wrappedArgUrl ].join('&') const billingId = generateUUID(); diff --git a/modules/neuwoRtdProvider.md b/modules/neuwoRtdProvider.md index 2adead66d4e..fb52734d451 100644 --- a/modules/neuwoRtdProvider.md +++ b/modules/neuwoRtdProvider.md @@ -33,6 +33,8 @@ pbjs.setConfig({realTimeData: { dataProviders: [ neuwoDataProvider ]}}) # Testing +`gulp test --modules=rtdModule,neuwoRtdProvider` + ## Add development tools if necessary - Install node for npm diff --git a/modules/newspassid.md b/modules/newspassid.md new file mode 100644 index 00000000000..6fa709e5ba6 --- /dev/null +++ b/modules/newspassid.md @@ -0,0 +1,76 @@ +--- +Module Name: NewspassId Bidder Adapter +Module Type: Bidder Adapter +Maintainer: techsupport@newspassid.com +layout: bidder +title: Newspass ID +description: LMC Newspass ID Prebid JS Bidder Adapter +biddercode: newspassid +gdpr_supported: false +gvl_id: none +usp_supported: true +coppa_supported: false +schain_supported: true +dchain_supported: false +userIds: criteo, id5Id, tdid, identityLink, liveIntentId, parrableId, pubCommonId, lotamePanoramaId, sharedId, fabrickId +media_types: banner +safeframes_ok: true +deals_supported: true +floors_supported: false +fpd_supported: false +pbjs: true +pbs: false +prebid_member: false +multiformat_supported: will-bid-on-any +--- + +### Description + +LMC Newspass ID Prebid JS Bidder Adapter that connects to the NewspassId demand source(s). + +The Newspass bid adapter supports Banner mediaTypes ONLY. +This is intended for USA audiences only, and does not support GDPR + + +### Bid Params + +{: .table .table-bordered .table-striped } + +| Name | Scope | Description | Example | Type | +|-----------|----------|---------------------------|------------|----------| +| `siteId` | required | The site ID. | `"NPID0000001"` | `string` | +| `publisherId` | required | The publisher ID. | `"4204204201"` | `string` | +| `placementId` | required | The placement ID. | `"0420420421"` | `string` | +| `customData` | optional | publisher key-values used for targeting | `[{"settings":{},"targeting":{"key1": "value1", "key2": "value2"}}], ` | `array` | + +### Test Parameters + + +A test ad unit that will consistently return test creatives: + +``` + +//Banner adUnit + +adUnits = [{ + code: 'id-of-your-banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'newspassid', + params: { + publisherId: 'NEWSPASS0001', /* an ID to identify the publisher account - required */ + siteId: '4204204201', /* An ID used to identify a site within a publisher account - required */ + placementId: '8000000015', /* an ID used to identify the piece of inventory - required - for appnexus test use 13144370. */ + customData: [{"settings": {}, "targeting": {"key": "value", "key2": ["value1", "value2"]}}],/* optional array with 'targeting' placeholder for passing publisher specific key-values for targeting. */ + } + }] + }]; +``` + +### Note: + +Please contact us at techsupport@newspassid.com for any assistance testing your implementation before going live into production. diff --git a/modules/newspassidBidAdapter.js b/modules/newspassidBidAdapter.js index b440edc2beb..2a4b2da186b 100644 --- a/modules/newspassidBidAdapter.js +++ b/modules/newspassidBidAdapter.js @@ -1,15 +1,24 @@ -import {contains, deepAccess, deepSetValue, isArray, logError, logInfo, logWarn, parseUrl} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import { + logInfo, + logError, + deepAccess, + logWarn, + deepSetValue, + isArray, + contains, + parseUrl, + generateUUID +} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {getPriceBucketString} from '../src/cpmBucketManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; - const BIDDER_CODE = 'newspassid'; const ORIGIN = 'https://bidder.newspassid.com' // applies only to auction & cookie const AUCTIONURI = '/openrtb2/auction'; const NEWSPASSCOOKIESYNC = '/static/load-cookie.html'; -const NEWSPASSVERSION = '1.0.1'; +const NEWSPASSVERSION = '1.1.4'; export const spec = { version: NEWSPASSVERSION, code: BIDDER_CODE, @@ -155,7 +164,7 @@ export const spec = { let placementId = placementIdOverrideFromGetParam || this.getPlacementId(npBidRequest); // prefer to use a valid override param, else the bidRequest placement Id obj.id = npBidRequest.bidId; // this causes an error if we change it to something else, even if you update the bidRequest object: "WARNING: Bidder newspass made bid for unknown request ID: mb7953.859498327448. Ignoring." obj.tagid = placementId; - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().page); obj.secure = parsed.protocol === 'https' ? 1 : 0; let arrBannerSizes = []; if (!npBidRequest.hasOwnProperty('mediaTypes')) { @@ -189,7 +198,6 @@ export const spec = { deepSetValue(obj, 'ext.prebid', {'storedrequest': {'id': placementId}}); obj.ext['newspassid'] = {}; obj.ext['newspassid'].adUnitCode = npBidRequest.adUnitCode; // eg. 'mpu' - obj.ext['newspassid'].transactionId = npBidRequest.transactionId; // this is the transactionId PER adUnit, common across bidders for this unit if (npBidRequest.params.hasOwnProperty('customData')) { obj.ext['newspassid'].customData = npBidRequest.params.customData; } @@ -216,6 +224,10 @@ export const spec = { if (!schain && deepAccess(npBidRequest, 'schain')) { schain = npBidRequest.schain; } + let gpid = deepAccess(npBidRequest, 'ortb2Imp.ext.gpid'); + if (gpid) { + deepSetValue(obj, 'ext.gpid', gpid); + } return obj; }); let extObj = {}; @@ -240,7 +252,7 @@ export const spec = { let userExtEids = deepAccess(validBidRequests, '0.userIdAsEids', []); // generate the UserIDs in the correct format for UserId module npRequest.site = { 'publisher': {'id': htmlParams.publisherId}, - 'page': getRefererInfo().page, + 'page': this.getRefererInfo().page, 'id': htmlParams.siteId }; npRequest.test = config.getConfig('debug') ? 1 : 0; @@ -259,12 +271,9 @@ export const spec = { } if (singleRequest) { logInfo('buildRequests starting to generate response for a single request'); - // TODO: fix auctionId & transactionId leak: https://github.com/prebid/Prebid.js/issues/9781 - npRequest.id = bidderRequest.auctionId; // Unique ID of the bid request, provided by the exchange. - npRequest.auctionId = bidderRequest.auctionId; // not sure if this should be here? + npRequest.id = generateUUID(); // Unique ID of the bid request, provided by the exchange. (REQUIRED) npRequest.imp = tosendtags; npRequest.ext = extObj; - deepSetValue(npRequest, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(npRequest, 'user.ext.eids', userExtEids); var ret = { method: 'POST', @@ -280,12 +289,9 @@ export const spec = { let arrRet = tosendtags.map(imp => { logInfo('buildRequests starting to generate non-single response, working on imp : ', imp); let npRequestSingle = Object.assign({}, npRequest); - imp.ext['newspassid'].pageAuctionId = bidderRequest['auctionId']; // make a note in the ext object of what the original auctionId was, in the bidderRequest object - npRequestSingle.id = imp.ext['newspassid'].transactionId; // Unique ID of the bid request, provided by the exchange. - npRequestSingle.auctionId = imp.ext['newspassid'].transactionId; // not sure if this should be here? + npRequestSingle.id = generateUUID(); npRequestSingle.imp = [imp]; npRequestSingle.ext = extObj; - deepSetValue(npRequestSingle, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(npRequestSingle, 'user.ext.eids', userExtEids); logInfo('buildRequests RequestSingle (for non-single) = ', npRequestSingle); return { @@ -305,6 +311,7 @@ export const spec = { logInfo(`interpretResponse time: ${startTime}. buildRequests done -> interpretResponse start was ${startTime - this.propertyBag.buildRequestsEnd}ms`); logInfo(`serverResponse, request`, JSON.parse(JSON.stringify(serverResponse)), JSON.parse(JSON.stringify(request))); serverResponse = serverResponse.body || {}; + let aucId = serverResponse.id; // this will be correct for single requests and non-single if (!serverResponse.hasOwnProperty('seatbid')) { return []; } @@ -351,7 +358,7 @@ export const spec = { logInfo(`newspassid.enhancedAdserverTargeting is set to false, no per-bid keys will be sent to adserver.`); } let {seat: winningSeat, bid: winningBid} = this.getWinnerForRequestBid(thisBid.bidId, serverResponse.seatbid); - adserverTargeting['np_auc_id'] = String(request.bidderRequest.auctionId); + adserverTargeting['np_auc_id'] = String(aucId); adserverTargeting['np_winner'] = String(winningSeat); adserverTargeting['np_bid'] = 'true'; if (enhancedAdserverTargeting) { @@ -490,10 +497,29 @@ export const spec = { return null; }, getGetParametersAsObject() { - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().location); // was getRefererInfo().page but this is not backwards compatible logInfo('getGetParametersAsObject found:', parsed.search); return parsed.search; }, + getRefererInfo() { + if (getRefererInfo().hasOwnProperty('location')) { + logInfo('FOUND location on getRefererInfo OK (prebid >= 7); will use getRefererInfo for location & page'); + return getRefererInfo(); + } else { + logInfo('DID NOT FIND location on getRefererInfo (prebid < 7); will use legacy code that ALWAYS worked reliably to get location & page ;-)'); + try { + return { + page: top.location.href, + location: top.location.href + }; + } catch (e) { + return { + page: window.location.href, + location: window.location.href + }; + } + } + }, blockTheRequest() { let npRequest = config.getConfig('newspassid.np_request'); if (typeof npRequest == 'boolean' && !npRequest) { diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index cb660ad9fd6..0cbe954175c 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -1,8 +1,7 @@ import { _each, createTrackPixelHtml, - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, getDefinedParams, getWindowTop, isArray, diff --git a/modules/nextrollBidAdapter.js b/modules/nextrollBidAdapter.js index 0dd4b334f6e..eab174d22dd 100644 --- a/modules/nextrollBidAdapter.js +++ b/modules/nextrollBidAdapter.js @@ -1,6 +1,5 @@ import { - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, isArray, isFn, isNumber, diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js index 671cc800980..c65544936fa 100644 --- a/modules/nexx360BidAdapter.js +++ b/modules/nexx360BidAdapter.js @@ -13,7 +13,7 @@ const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstre const BIDDER_CODE = 'nexx360'; const REQUEST_URL = 'https://fast.nexx360.io/booster'; const PAGE_VIEW_ID = generateUUID(); -const BIDDER_VERSION = '2.0'; +const BIDDER_VERSION = '3.0'; const GVLID = 965; const NEXXID_KEY = 'nexx360_storage'; @@ -190,9 +190,15 @@ function interpretResponse(serverResponse) { demandSource: bid.ext.ssp, }, }; - if (allowAlternateBidderCodes) response.bidderCode = `n360-${bid.ext.ssp}`; + if (allowAlternateBidderCodes) response.bidderCode = `n360_${bid.ext.ssp}`; - if (bid.ext.mediaType === BANNER) response.adUrl = bid.ext.adUrl; + if (bid.ext.mediaType === BANNER) { + if (bid.adm) { + response.ad = bid.adm; + } else { + response.adUrl = bid.ext.adUrl; + } + } if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType)) response.vastXml = bid.ext.vastXml; if (bid.ext.mediaType === OUTSTREAM) { diff --git a/modules/nobidAnalyticsAdapter.js b/modules/nobidAnalyticsAdapter.js new file mode 100644 index 00000000000..27ec1cd9451 --- /dev/null +++ b/modules/nobidAnalyticsAdapter.js @@ -0,0 +1,199 @@ +import {deepClone, logError, getParameterByName} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import {getStorageManager} from '../src/storageManager.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager from '../src/adapterManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; + +const VERSION = '1.0.4'; +const MODULE_NAME = 'nobidAnalyticsAdapter'; +const ANALYTICS_DATA_NAME = 'analytics.nobid.io'; +const RETENTION_SECONDS = 7 * 24 * 3600; +const TEST_ALLOCATION_PERCENTAGE = 5; // dont block 5% of the time; +window.nobidAnalyticsVersion = VERSION; +const analyticsType = 'endpoint'; +const url = 'localhost:8383/event'; +const GVLID = 816; +const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME, moduleType: MODULE_TYPE_ANALYTICS}); +const { + EVENTS: { + AUCTION_INIT, + BID_REQUESTED, + BID_TIMEOUT, + BID_RESPONSE, + BID_WON, + AUCTION_END, + AD_RENDER_SUCCEEDED + } +} = CONSTANTS; +function log (msg) { + // eslint-disable-next-line no-console + console.log(`%cNoBid Analytics ${VERSION}`, 'padding: 2px 8px 2px 8px; background-color:#f50057; color: white', msg); +} +function isJson (str) { + return str && str.startsWith('{') && str.endsWith('}'); +} +function isExpired (data, retentionSeconds) { + retentionSeconds = retentionSeconds || RETENTION_SECONDS; + if (data.ts + retentionSeconds * 1000 < Date.now()) return true; + return false; +} +function sendEvent (event, eventType) { + function resolveEndpoint() { + var ret = 'https://carbon-nv.servenobids.com/admin/status'; + var env = (typeof getParameterByName === 'function') && (getParameterByName('nobid-env')); + env = window.location.href.indexOf('nobid-env=dev') > 0 ? 'dev' : env; + if (!env) ret = 'https://carbon-nv.servenobids.com'; + else if (env == 'dev') ret = 'https://localhost:8383'; + return ret; + } + if (!nobidAnalytics.initOptions || !nobidAnalytics.initOptions.siteId || !event) return; + if (nobidAnalytics.isAnalyticsDisabled()) { + log('NoBid Analytics is Disabled'); + return; + } + try { + const endpoint = `${resolveEndpoint()}/event/${eventType}?pubid=${nobidAnalytics.initOptions.siteId}`; + ajax(endpoint, + function (response) { + try { + nobidAnalytics.processServerResponse(response); + } catch (e) { + logError(e); + } + }, + JSON.stringify(event), + { + contentType: 'application/json', + method: 'POST' + } + ); + } catch (err) { + log(`Sending event error ${err}`); + } +} +function cleanupObjectAttributes (obj, attributes) { + if (!obj) return; + if (Array.isArray(obj)) { + obj.forEach(item => { + Object.keys(item).forEach(attr => { if (!attributes.includes(attr)) delete item[attr] }); + }); + } else Object.keys(obj).forEach(attr => { if (!attributes.includes(attr)) delete obj[attr] }); +} +function sendBidWonEvent (event, eventType) { + const data = deepClone(event); + cleanupObjectAttributes(data, ['bidderCode', 'size', 'statusMessage', 'adId', 'requestId', 'mediaType', 'adUnitCode', 'cpm', 'timeToRespond']); + if (nobidAnalytics.topLocation) data.topLocation = nobidAnalytics.topLocation; + sendEvent(data, eventType); +} +function sendAuctionEndEvent (event, eventType) { + if (event?.bidderRequests?.length > 0 && event?.bidderRequests[0]?.refererInfo?.topmostLocation) { + nobidAnalytics.topLocation = event.bidderRequests[0].refererInfo.topmostLocation; + } + const data = deepClone(event); + + cleanupObjectAttributes(data, ['timestamp', 'timeout', 'auctionId', 'bidderRequests', 'bidsReceived']); + if (data) cleanupObjectAttributes(data.bidderRequests, ['bidderCode', 'bidderRequestId', 'bids', 'refererInfo']); + if (data) cleanupObjectAttributes(data.bidsReceived, ['bidderCode', 'width', 'height', 'adUnitCode', 'statusMessage', 'requestId', 'mediaType', 'cpm']); + if (data) cleanupObjectAttributes(data.noBids, ['bidder', 'sizes', 'bidId']); + if (data.bidderRequests) cleanupObjectAttributes(data.bidderRequests.bids, ['mediaTypes', 'adUnitCode', 'sizes', 'bidId']); + if (data.bidderRequests) cleanupObjectAttributes(data.bidderRequests.refererInfo, ['topmostLocation']); + sendEvent(data, eventType); +} +function auctionInit (event) { + if (event?.bidderRequests?.length > 0 && event?.bidderRequests[0]?.refererInfo?.topmostLocation) { + nobidAnalytics.topLocation = event.bidderRequests[0].refererInfo.topmostLocation; + } +} +let nobidAnalytics = Object.assign(adapter({url, analyticsType}), { + track({ eventType, args }) { + switch (eventType) { + case AUCTION_INIT: + auctionInit(args); + break; + case BID_REQUESTED: + break; + case BID_RESPONSE: + break; + case BID_WON: + sendBidWonEvent(args, eventType); + break; + case BID_TIMEOUT: + break; + case AUCTION_END: + sendAuctionEndEvent(args, eventType); + break; + case AD_RENDER_SUCCEEDED: + break; + default: + break; + } + } +}); + +nobidAnalytics = { + ...nobidAnalytics, + originEnableAnalytics: nobidAnalytics.enableAnalytics, // save the base class function + enableAnalytics: function (config) { // override enableAnalytics so we can get access to the config passed in from the page + if (!config.options.siteId) { + logError('NoBid Analytics - siteId parameter is not defined. Analytics won\'t work'); + return; + } + this.initOptions = config.options; + this.originEnableAnalytics(config); // call the base class function + }, + retentionSeconds: RETENTION_SECONDS, + isExpired (data) { + return isExpired(data, this.retentionSeconds); + }, + isAnalyticsDisabled () { + let stored = storage.getDataFromLocalStorage(ANALYTICS_DATA_NAME); + if (!isJson(stored)) return false; + stored = JSON.parse(stored); + if (this.isExpired(stored)) return false; + return stored.disabled; + }, + processServerResponse (response) { + if (!isJson(response)) return; + const resp = JSON.parse(response); + storage.setDataInLocalStorage(ANALYTICS_DATA_NAME, JSON.stringify({ ...resp, ts: Date.now() })); + } +} + +adapterManager.registerAnalyticsAdapter({ + adapter: nobidAnalytics, + code: 'nobidAnalytics', + gvlid: GVLID +}); +window.nobidCarbonizer = { + getStoredLocalData: function () { + return storage.getDataFromLocalStorage(ANALYTICS_DATA_NAME); + }, + isActive: function () { + let stored = storage.getDataFromLocalStorage(ANALYTICS_DATA_NAME); + if (!isJson(stored)) return false; + stored = JSON.parse(stored); + if (isExpired(stored, nobidAnalytics.retentionSeconds)) return false; + return stored.carbonizer_active || false; + }, + carbonizeAdunits: function (adunits, skipTestGroup) { + function carbonizeAdunit (adunit) { + let stored = storage.getDataFromLocalStorage(ANALYTICS_DATA_NAME); + if (!isJson(stored)) return; + stored = JSON.parse(stored); + if (isExpired(stored, nobidAnalytics.retentionSeconds)) return; + const carbonizerBidders = stored.bidders || []; + const allowedBidders = adunit.bids.filter(rec => carbonizerBidders.includes(rec.bidder)); + adunit.bids = allowedBidders; + } + if (this.isActive()) { + // 5% of the time do not block; + if (!skipTestGroup && Math.floor(Math.random() * 101) <= TEST_ALLOCATION_PERCENTAGE) return; + adunits.forEach(adunit => { + carbonizeAdunit(adunit); + }); + } + } +}; +export default nobidAnalytics; diff --git a/modules/nobidAnalyticsAdapter.md b/modules/nobidAnalyticsAdapter.md new file mode 100644 index 00000000000..92b9bdbb3cb --- /dev/null +++ b/modules/nobidAnalyticsAdapter.md @@ -0,0 +1,38 @@ +# Overview +Module Name: NoBid Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: [nobid.io](https://nobid.io) + + +# NoBid Analytics Registration + +The NoBid Analytics Adapter is free to use during our Beta period, but requires a simple registration with NoBid. Please visit [www.nobid.io](https://www.nobid.io/contact-1/) to sign up and request your NoBid Site ID to get started. If you're already using the NoBid Prebid Adapter, you may use your existing Site ID with the NoBid Analytics Adapter. + +The NoBid privacy policy is at [nobid.io/privacy-policy](https://www.nobid.io/privacy-policy/). + +## NoBid Analytics Configuration + +First, make sure to add the NoBid Analytics submodule to your Prebid.js package with: + +``` +gulp build --modules=...,nobidAnalyticsAdapter... +``` + +The following configuration parameters are available: + +```javascript +pbjs.enableAnalytics({ + provider: 'nobidAnalytics', + options: { + siteId: 123 // change to the Site ID you received from NoBid + } +}); +``` + +{: .table .table-bordered .table-striped } +| Parameter | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| provider | Required | String | The name of this module: `nobidAnalytics` | `nobidAnalytics` | +| options.siteId | Required | Number | This is the NoBid Site ID Number obtained from registering with NoBid. | `1234` | diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index 7bf1c4b80db..68010b32b37 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -3,7 +3,7 @@ import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import { hasPurpose1Consent } from '../src/utils/gpdr.js'; const GVLID = 816; const BIDDER_CODE = 'nobid'; @@ -63,6 +63,19 @@ function nobidBuildRequests(bids, bidderRequest) { } return uspConsent; } + var gppConsent = function(bidderRequest) { + let gppConsent = null; + if (bidderRequest?.gppConsent?.gppString && bidderRequest?.gppConsent?.applicableSections) { + gppConsent = {}; + gppConsent.gpp = bidderRequest.gppConsent.gppString; + gppConsent.gpp_sid = Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections : []; + } else if (bidderRequest?.ortb2?.regs?.gpp && bidderRequest?.ortb2.regs?.gpp_sid) { + gppConsent = {}; + gppConsent.gpp = bidderRequest.ortb2.regs.gpp; + gppConsent.gpp_sid = Array.isArray(bidderRequest.ortb2.regs.gpp_sid) ? bidderRequest.ortb2.regs.gpp_sid : []; + } + return gppConsent; + } var schain = function(bids) { if (bids && bids.length > 0) { return bids[0].schain @@ -145,6 +158,9 @@ function nobidBuildRequests(bids, bidderRequest) { if (cop) state['coppa'] = cop; const eids = getEIDs(deepAccess(bids, '0.userIdAsEids')); if (eids && eids.length > 0) state['eids'] = eids; + const gpp = gppConsent(bidderRequest); + if (gpp?.gpp) state['gpp'] = gpp.gpp; + if (gpp?.gpp_sid) state['gpp_sid'] = gpp.gpp_sid; if (bidderRequest && bidderRequest.ortb2) state['ortb2'] = bidderRequest.ortb2; return state; }; @@ -230,9 +246,9 @@ function nobidBuildRequests(bids, bidderRequest) { siteId = (typeof bid.params['siteId'] != 'undefined' && bid.params['siteId']) ? bid.params['siteId'] : siteId; var placementId = bid.params['placementId']; - var adType = 'banner'; + let adType = 'banner'; const videoMediaType = deepAccess(bid, 'mediaTypes.video'); - const context = deepAccess(bid, 'mediaTypes.video.context'); + const context = deepAccess(bid, 'mediaTypes.video.context') || ''; if (bid.mediaType === VIDEO || (videoMediaType && (context === 'instream' || context === 'outstream'))) { adType = 'video'; } @@ -246,7 +262,8 @@ function nobidBuildRequests(bids, bidderRequest) { placementId: placementId, ad_type: adType, params: bid.params, - floor: floor + floor: floor, + ctx: context }, adunits); } @@ -369,6 +386,7 @@ export const spec = { function resolveEndpoint() { var ret = 'https://ads.servenobid.com/'; var env = (typeof getParameterByName === 'function') && (getParameterByName('nobid-env')); + env = window.location.href.indexOf('nobid-env=dev') > 0 ? 'dev' : env; if (!env) ret = 'https://ads.servenobid.com/'; else if (env == 'beta') ret = 'https://beta.servenobid.com/'; else if (env == 'dev') ret = '//localhost:8282/'; @@ -421,7 +439,7 @@ 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, usPrivacy) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, usPrivacy, gppConsent) { if (syncOptions.iframeEnabled) { let params = ''; if (gdprConsent && typeof gdprConsent.consentString === 'string') { @@ -437,6 +455,12 @@ export const spec = { else params += '?'; params += 'usp_consent=' + usPrivacy; } + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + if (params.length > 0) params += '&'; + else params += '?'; + params += 'gpp=' + encodeURIComponent(gppConsent.gppString); + params += 'gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(',')); + } return [{ type: 'iframe', url: 'https://public.servenobid.com/sync.html' + params diff --git a/modules/nobidBidAdapter.md b/modules/nobidBidAdapter.md index 4449ad5c88b..e2f1c75e782 100644 --- a/modules/nobidBidAdapter.md +++ b/modules/nobidBidAdapter.md @@ -7,6 +7,7 @@ hide: true media_types: banner, video gdpr_supported: true usp_supported: true +gpp_supported: true --- ### Bid Params diff --git a/modules/novatiqIdSystem.js b/modules/novatiqIdSystem.js index 7a801a945ae..7eced81d35e 100644 --- a/modules/novatiqIdSystem.js +++ b/modules/novatiqIdSystem.js @@ -259,6 +259,18 @@ export const novatiqIdSubmodule = { srcId = configParams.sourceid; } return srcId; + }, + eids: { + 'novatiq': { + getValue: function(data) { + if (data.snowflake.id === undefined) { + return data.snowflake; + } + + return data.snowflake.id; + }, + source: 'novatiq.com', + }, } }; submodule('userId', novatiqIdSubmodule); diff --git a/modules/oguryBidAdapter.js b/modules/oguryBidAdapter.js index 718ae44c83c..9937391f6e7 100644 --- a/modules/oguryBidAdapter.js +++ b/modules/oguryBidAdapter.js @@ -1,9 +1,10 @@ 'use strict'; import {BANNER} from '../src/mediaTypes.js'; -import {getAdUnitSizes, getWindowSelf, getWindowTop, isFn, logWarn} from '../src/utils.js'; +import {getWindowSelf, getWindowTop, isFn, logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ajax} from '../src/ajax.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'ogury'; const GVLID = 31; @@ -11,7 +12,7 @@ const DEFAULT_TIMEOUT = 1000; const BID_HOST = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_MONITORING_HOST = 'https://ms-ads-monitoring-events.presage.io'; const MS_COOKIE_SYNC_DOMAIN = 'https://ms-cookie-sync.presage.io'; -const ADAPTER_VERSION = '1.4.1'; +const ADAPTER_VERSION = '1.6.0'; function getClientWidth() { const documentElementClientWidth = window.top.document.documentElement.clientWidth @@ -46,22 +47,35 @@ function isBidRequestValid(bid) { } function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { - if (!syncOptions.pixelEnabled) return []; + const consent = (gdprConsent && gdprConsent.consentString) || ''; - return [ - { - type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/v1/init-sync/bid-switch?iab_string=${(gdprConsent && gdprConsent.consentString) || ''}&source=prebid` - }, - { - type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/ttd/init-sync?iab_string=${(gdprConsent && gdprConsent.consentString) || ''}&source=prebid` - }, - { - type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/xandr/init-sync?iab_string=${(gdprConsent && gdprConsent.consentString) || ''}&source=prebid` - } - ] + if (syncOptions.iframeEnabled) { + return [ + { + type: 'iframe', + url: `${MS_COOKIE_SYNC_DOMAIN}/user-sync.html?gdpr_consent=${consent}&source=prebid` + } + ]; + } + + if (syncOptions.pixelEnabled) { + return [ + { + type: 'image', + url: `${MS_COOKIE_SYNC_DOMAIN}/v1/init-sync/bid-switch?iab_string=${consent}&source=prebid` + }, + { + type: 'image', + url: `${MS_COOKIE_SYNC_DOMAIN}/ttd/init-sync?iab_string=${consent}&source=prebid` + }, + { + type: 'image', + url: `${MS_COOKIE_SYNC_DOMAIN}/xandr/init-sync?iab_string=${consent}&source=prebid` + } + ]; + } + + return []; } function buildRequests(validBidRequests, bidderRequest) { @@ -108,6 +122,13 @@ function buildRequests(validBidRequests, bidderRequest) { openRtbBidRequestBanner.site.id = bidRequest.params.assetKey; const floor = getFloor(bidRequest); + if (bidRequest.userId) { + openRtbBidRequestBanner.user.ext.uids = bidRequest.userId + } + if (bidRequest.userIdAsEids) { + openRtbBidRequestBanner.user.ext.eids = bidRequest.userIdAsEids + } + openRtbBidRequestBanner.imp.push({ id: bidRequest.bidId, tagid: bidRequest.params.adUnitId, diff --git a/modules/oneKeyIdSystem.js b/modules/oneKeyIdSystem.js index 8d8d594b533..699a7a6ab95 100644 --- a/modules/oneKeyIdSystem.js +++ b/modules/oneKeyIdSystem.js @@ -72,6 +72,32 @@ export const oneKeyIdSubmodule = { return { callback: getIdsAndPreferences }; + }, + eids: { + 'oneKeyData': { + getValue: function(data) { + if (data && Array.isArray(data.identifiers) && data.identifiers[0]) { + return data.identifiers[0].value; + } + }, + source: 'paf', + atype: 1, + getEidExt: function(data) { + if (data && data.preferences) { + return {preferences: data.preferences}; + } + }, + getUidExt: function(data) { + if (data && Array.isArray(data.identifiers) && data.identifiers[0]) { + const id = data.identifiers[0]; + return { + version: id.version, + type: id.type, + source: id.source + }; + } + } + } } }; diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index 724a53a3095..eb381727a3b 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -70,6 +70,9 @@ function buildRequests(validBidRequests, bidderRequest) { if (bidderRequest && bidderRequest.uspConsent) { payload.usPrivacy = bidderRequest.uspConsent; } + if (bidderRequest && bidderRequest.ortb2) { + payload.ortb2 = bidderRequest.ortb2; + } if (validBidRequests && validBidRequests.length !== 0 && validBidRequests[0].userIdAsEids) { payload.userId = validBidRequests[0].userIdAsEids; } @@ -264,9 +267,8 @@ function setGeneralInfo(bidRequest) { this['adUnitCode'] = bidRequest.adUnitCode; this['bidId'] = bidRequest.bidId; this['bidderRequestId'] = bidRequest.bidderRequestId; - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - this['auctionId'] = bidRequest.auctionId; - this['transactionId'] = bidRequest.ortb2Imp?.ext?.tid; + this['auctionId'] = deepAccess(bidRequest, 'ortb2.source.tid'); + this['transactionId'] = deepAccess(bidRequest, 'ortb2Imp.ext.tid'); this['gpid'] = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); this['pubId'] = params.pubId; this['ext'] = params.ext; diff --git a/modules/onomagicBidAdapter.js b/modules/onomagicBidAdapter.js index edab625e541..78f00153a8b 100644 --- a/modules/onomagicBidAdapter.js +++ b/modules/onomagicBidAdapter.js @@ -1,7 +1,6 @@ import { _each, - createTrackPixelHtml, - getBidIdParameter, + createTrackPixelHtml, getBidIdParameter, getUniqueIdentifierStr, getWindowSelf, getWindowTop, diff --git a/modules/open8BidAdapter.js b/modules/open8BidAdapter.js index 5fa1dd0a143..49523926c0e 100644 --- a/modules/open8BidAdapter.js +++ b/modules/open8BidAdapter.js @@ -1,8 +1,9 @@ import { Renderer } from '../src/Renderer.js'; import {ajax} from '../src/ajax.js'; -import { createTrackPixelHtml, getBidIdParameter, logError, logWarn, tryAppendQueryString } from '../src/utils.js'; +import {createTrackPixelHtml, getBidIdParameter, logError, logWarn} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'open8'; const URL = 'https://as.vt.open8.com/v1/control/prebid'; diff --git a/modules/openwebBidAdapter.js b/modules/openwebBidAdapter.js index 296bfc682f1..547447039da 100644 --- a/modules/openwebBidAdapter.js +++ b/modules/openwebBidAdapter.js @@ -1,8 +1,9 @@ -import {convertTypes, deepAccess, flatten, isArray, isNumber, parseSizesInput} from '../src/utils.js'; +import {deepAccess, flatten, isArray, isNumber, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find} from '../src/polyfill.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const ENDPOINT = 'https://ghb.spotim.market/v2/auction'; const BIDDER_CODE = 'openweb'; diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 03423a028b4..0f8bee213f7 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -4,6 +4,7 @@ import * as utils from '../src/utils.js'; import {mergeDeep} from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const bidderConfig = 'hb_pb_ortb'; const bidderVersion = '2.0'; @@ -12,6 +13,7 @@ export const SYNC_URL = 'https://u.openx.net/w/1.0/pd'; export const DEFAULT_PH = '2d1251ae-7f3a-47cf-bd2a-2f288854a0ba'; export const spec = { code: 'openx', + gvlid: 69, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid, buildRequests, @@ -48,7 +50,8 @@ const converter = ortbConverter({ mergeDeep(req, { at: 1, ext: { - bc: `${bidderConfig}_${bidderVersion}` + bc: `${bidderConfig}_${bidderVersion}`, + pv: '$prebid.version$' } }) const bid = context.bidRequests[0]; @@ -149,7 +152,7 @@ const converter = ortbConverter({ }); function transformBidParams(params, isOpenRtb) { - return utils.convertTypes({ + return convertTypes({ 'unit': 'string', 'customFloor': 'number' }, params); diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index e721fb85fd7..131ba0bc1f2 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -70,7 +70,7 @@ const NATIVE_DEFAULTS = { export const spec = { code: BIDDER_CODE, - + gvlid: 1135, // short code aliases: ['opera'], @@ -232,13 +232,6 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { test: config.getConfig('debug') ? 1 : 0, imp: createImp(bidRequest), device: getDevice(), - site: { - id: String(deepAccess(bidRequest, 'params.publisherId')), - // TODO: does the fallback make sense here? - domain: bidderRequest?.refererInfo?.domain || window.location.host, - page: bidderRequest?.refererInfo?.page, - ref: bidderRequest?.refererInfo?.ref || '', - }, at: 1, bcat: getBcat(bidRequest), cur: [DEFAULT_CURRENCY], @@ -250,6 +243,7 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { buyeruid: getUserId(bidRequest) } } + fulfillInventoryInfo(payload, bidRequest, bidderRequest); const gdprConsent = deepAccess(bidderRequest, 'gdprConsent'); if (!!gdprConsent && gdprConsent.gdprApplies) { @@ -680,6 +674,11 @@ function mapNativeImage(image, type) { * @returns {String} userId */ function getUserId(bidRequest) { + let operaId = deepAccess(bidRequest, 'userId.operaId'); + if (operaId) { + return operaId; + } + let sharedId = deepAccess(bidRequest, 'userId.sharedid.id'); if (sharedId) { return sharedId; @@ -759,6 +758,38 @@ function getDevice() { return device; } +/** + * Fulfill inventory info + * + * @param payload + * @param bidRequest + * @param bidderRequest + */ +function fulfillInventoryInfo(payload, bidRequest, bidderRequest) { + let info = deepAccess(bidRequest, 'params.site'); + // 1.If the inventory info for site specified, use the site object provided in params. + let key = 'site'; + if (!isPlainObject(info)) { + info = deepAccess(bidRequest, 'params.app'); + if (isPlainObject(info)) { + // 2.If the inventory info for app specified, use the app object provided in params. + key = 'app'; + } else { + // 3.Otherwise, we use site by default. + info = {}; + } + } + // Fulfill key parameters. + info.id = String(deepAccess(bidRequest, 'params.publisherId')); + info.domain = info.domain || bidderRequest?.refererInfo?.domain || window.location.host; + if (key === 'site') { + info.ref = info.ref || bidderRequest?.refererInfo?.ref || ''; + info.page = info.page || bidderRequest?.refererInfo?.page; + } + + payload[key] = info; +} + /** * Get browser language * diff --git a/modules/operaadsBidAdapter.md b/modules/operaadsBidAdapter.md index 709c67a04a7..6f13eebd7d5 100644 --- a/modules/operaadsBidAdapter.md +++ b/modules/operaadsBidAdapter.md @@ -14,41 +14,43 @@ Module that connects to OperaAds's demand sources ## Bid Parameters -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `placementId` | required | String | The Placement Id provided by Opera Ads. | `s5340077725248` -| `endpointId` | required | String | The Endpoint Id provided by Opera Ads. | `ep3425464070464` -| `publisherId` | required | String | The Publisher Id provided by Opera Ads. | `pub3054952966336` -| `bcat` | optional | String or String[] | The bcat value. | `IAB9-31` +| Name | Scope | Type | Description | Example | +|---------------|----------|--------------------|-----------------------------------------|-------------------------------------------------| +| `placementId` | required | String | The Placement Id provided by Opera Ads. | `s5340077725248` | +| `endpointId` | required | String | The Endpoint Id provided by Opera Ads. | `ep3425464070464` | +| `publisherId` | required | String | The Publisher Id provided by Opera Ads. | `pub3054952966336` | +| `bcat` | optional | String or String[] | The bcat value. | `IAB9-31` | +| `site` | optional | Object | The site information. | `{"name": "my_site", "domain": "www.test.com"}` | +| `app` | optional | Object | The app information. | `{"name": "my_app", "ver": "1.1.0"}` | ### Bid Video Parameters Set these parameters to `bid.mediaTypes.video`. -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `context` | optional | String | `instream` or `outstream`. | `instream` -| `mimes` | optional | String[] | Content MIME types supported. | `['video/mp4']` -| `playerSize` | optional | Number[] or Number[][] | Video player size in device independent pixels | `[[640, 480]]` -| `protocols` | optional | Number[] | Array of supported video protocls. | `[1, 2, 3, 4, 5, 6, 7, 8]` -| `startdelay` | optional | Number | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | `0` -| `skip` | optional | Number | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes. | `1` -| `playbackmethod` | optional | Number[] | Playback methods that may be in use. | `[2]` -| `delivery` | optional | Number[] | Supported delivery methods. | `[1]` -| `api` | optional | Number[] | List of supported API frameworks for this impression. | `[1, 2, 5]` +| Name | Scope | Type | Description | Example | +|------------------|----------|------------------------|------------------------------------------------------------------------------------------|----------------------------| +| `context` | optional | String | `instream` or `outstream`. | `instream` | +| `mimes` | optional | String[] | Content MIME types supported. | `['video/mp4']` | +| `playerSize` | optional | Number[] or Number[][] | Video player size in device independent pixels | `[[640, 480]]` | +| `protocols` | optional | Number[] | Array of supported video protocls. | `[1, 2, 3, 4, 5, 6, 7, 8]` | +| `startdelay` | optional | Number | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | `0` | +| `skip` | optional | Number | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes. | `1` | +| `playbackmethod` | optional | Number[] | Playback methods that may be in use. | `[2]` | +| `delivery` | optional | Number[] | Supported delivery methods. | `[1]` | +| `api` | optional | Number[] | List of supported API frameworks for this impression. | `[1, 2, 5]` | ### Bid Native Parameters Set these parameters to `bid.nativeParams` or `bid.mediaTypes.native`. -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `title` | optional | Object | Config for native asset title. | `{required: true, len: 25}` -| `image` | optional | Object | Config for native asset image. | `{required: true, sizes: [[300, 250]], aspect_ratios: [{min_width: 300, min_height: 250, ratio_width: 1, ratio_height: 1}]}` -| `icon` | optional | Object | Config for native asset icon. | `{required: true, sizes: [[60, 60]], aspect_ratios: [{min_width: 60, min_height: 60, ratio_width: 1, ratio_height: 1}]}}` -| `sponsoredBy` | optional | Object | Config for native asset sponsoredBy. | `{required: true, len: 20}` -| `body` | optional | Object | Config for native asset body. | `{required: true, len: 200}` -| `cta` | optional | Object | Config for native asset cta. | `{required: true, len: 20}` +| Name | Scope | Type | Description | Example | +|---------------|----------|--------|--------------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| `title` | optional | Object | Config for native asset title. | `{required: true, len: 25}` | +| `image` | optional | Object | Config for native asset image. | `{required: true, sizes: [[300, 250]], aspect_ratios: [{min_width: 300, min_height: 250, ratio_width: 1, ratio_height: 1}]}` | +| `icon` | optional | Object | Config for native asset icon. | `{required: true, sizes: [[60, 60]], aspect_ratios: [{min_width: 60, min_height: 60, ratio_width: 1, ratio_height: 1}]}}` | +| `sponsoredBy` | optional | Object | Config for native asset sponsoredBy. | `{required: true, len: 20}` | +| `body` | optional | Object | Config for native asset body. | `{required: true, len: 200}` | +| `cta` | optional | Object | Config for native asset cta. | `{required: true, len: 20}` | ## Example @@ -127,7 +129,9 @@ var adUnits = [{ params: { placementId: 's5340077725248', endpointId: 'ep3425464070464', - publisherId: 'pub3054952966336' + publisherId: 'pub3054952966336', + // You might want to specify some application information here if the bid requests are from an application instead of a browser. + app: { 'name': 'my_app', 'bundle': 'test_bundle', 'store_url': 'www.some-store.com', 'ver': '1.1.0' } } }] }]; @@ -135,18 +139,18 @@ var adUnits = [{ ### User Ids -Opera Ads Bid Adapter uses `sharedId`, `pubcid` or `tdid`, please config at least one. +Opera Ads Bid Adapter uses `operaId`, please refer to [`Opera ID System`](./operaadsIdSystem.md). ```javascript pbjs.setConfig({ ..., userSync: { userIds: [{ - name: 'sharedId', + name: 'operaId', storage: { - name: '_sharedID', // name of the 1st party cookie - type: 'cookie', - expires: 30 + name: 'operaId', + type: 'html5', + expires: 14 } }] } diff --git a/modules/operaadsIdSystem.js b/modules/operaadsIdSystem.js new file mode 100644 index 00000000000..09dd8512a2b --- /dev/null +++ b/modules/operaadsIdSystem.js @@ -0,0 +1,106 @@ +/** + * This module adds operaId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/operaadsIdSystem + * @requires module:modules/userId + */ +import * as ajax from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { logMessage, logError } from '../src/utils.js'; + +const MODULE_NAME = 'operaId'; +const ID_KEY = MODULE_NAME; +const version = '1.0'; +const SYNC_URL = 'https://t.adx.opera.com/identity/'; +const AJAX_TIMEOUT = 300; +const AJAX_OPTIONS = {method: 'GET', withCredentials: true, contentType: 'application/json'}; + +function constructUrl(pairs) { + const queries = []; + for (let key in pairs) { + queries.push(`${key}=${encodeURIComponent(pairs[key])}`); + } + return `${SYNC_URL}?${queries.join('&')}`; +} + +function asyncRequest(url, cb) { + ajax.ajaxBuilder(AJAX_TIMEOUT)( + url, + { + success: response => { + try { + const jsonResponse = JSON.parse(response); + const { uid: operaId } = jsonResponse; + cb(operaId); + return; + } catch (e) { + logError(`${MODULE_NAME}: invalid response`, response); + } + cb(); + }, + error: (err) => { + logError(`${MODULE_NAME}: ID error response`, err); + cb(); + } + }, + null, + AJAX_OPTIONS + ); +} + +export const operaIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * @type {string} + */ + version, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param {string} id + * @returns {{'operaId': string}} + */ + decode: (id) => + id != null && id.length > 0 + ? { [ID_KEY]: id } + : undefined, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId(config, consentData) { + logMessage(`${MODULE_NAME}: start synchronizing opera uid`); + const params = (config && config.params) || {}; + if (typeof params.pid !== 'string' || params.pid.length == 0) { + logError(`${MODULE_NAME}: submodule requires a publisher ID to be defined`); + return; + } + + const { pid, syncUrl = SYNC_URL } = params; + const url = constructUrl(syncUrl, { publisherId: pid }); + + return { + callback: (cb) => { + asyncRequest(url, cb); + } + } + }, + + eids: { + 'operaId': { + source: 't.adx.opera.com', + atype: 1 + }, + } +}; + +submodule('userId', operaIdSubmodule); diff --git a/modules/operaadsIdSystem.md b/modules/operaadsIdSystem.md new file mode 100644 index 00000000000..288fb960b96 --- /dev/null +++ b/modules/operaadsIdSystem.md @@ -0,0 +1,52 @@ +# Opera ID System + +For help adding this module, please contact [adtech-prebid-group@opera.com](adtech-prebid-group@opera.com). + +### Prebid Configuration + +You should configure this module under your `userSync.userIds[]` configuration: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [ + { + name: "operaId", + storage: { + name: "operaId", + type: "html5", + expires: 14 + }, + params: { + pid: "your-pulisher-ID-here" + } + } + ] + } +}) +``` +
+ +| Param under `userSync.userIds[]` | Scope | Type | Description | Example | +| -------------------------------- | -------- | ------ | ----------------------------- | ----------------------------------------- | +| name | Required | string | ID for the operaId module | `"operaId"` | +| storage | Optional | Object | Settings for operaId storage | See [storage settings](#storage-settings) | +| params | Required | Object | Parameters for opreaId module | See [params](#params) | +
+ +### Params + +| Param under `params` | Scope | Type | Description | Example | +| -------------------- | -------- | ------ | ------------------------------ | --------------- | +| pid | Required | string | Publisher ID assigned by Opera | `"pub12345678"` | +
+ +### Storage Settings + +The following settings are suggested for the `storage` property in the `userSync.userIds[]` object: + +| Param under `storage` | Type | Description | Example | +| --------------------- | ------------- | -------------------------------------------------------------------------------- | ----------- | +| name | String | Where the ID will be stored | `"operaId"` | +| type | String | For best performance, this should be `"html5"` | `"html5"` | +| expires | Number <= 30 | number of days until the stored ID expires. **Must be less than or equal to 30** | `14` | \ No newline at end of file diff --git a/modules/optidigitalBidAdapter.js b/modules/optidigitalBidAdapter.js index a0fa641a424..9f27ae49d1e 100755 --- a/modules/optidigitalBidAdapter.js +++ b/modules/optidigitalBidAdapter.js @@ -1,12 +1,14 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { deepAccess, parseSizesInput, getAdUnitSizes } from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {deepAccess, parseSizesInput} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'optidigital'; const GVL_ID = 915; const ENDPOINT_URL = 'https://pbs.optidigital.com/bidder'; const USER_SYNC_URL_IFRAME = 'https://scripts.opti-digital.com/js/presync.html?endpoint=optidigital'; let CUR = 'USD'; +let isSynced = false; export const spec = { code: BIDDER_CODE, @@ -46,8 +48,6 @@ export const spec = { referrer: (bidderRequest.refererInfo && bidderRequest.refererInfo.page) ? bidderRequest.refererInfo.page : '', hb_version: '$prebid.version$', deviceWidth: document.documentElement.clientWidth, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: deepAccess(validBidRequests[0], 'auctionId'), bidderRequestId: deepAccess(validBidRequests[0], 'bidderRequestId'), publisherId: deepAccess(validBidRequests[0], 'params.publisherId'), imp: validBidRequests.map(bidRequest => buildImp(bidRequest, ortb2)), @@ -56,6 +56,10 @@ export const spec = { bapp: deepAccess(validBidRequests[0], 'params.bapp') || [] } + if (validBidRequests[0].auctionId) { + payload.auctionId = validBidRequests[0].auctionId; + } + if (validBidRequests[0].params.pageTemplate && validBidRequests[0].params.pageTemplate !== '') { payload.pageTemplate = validBidRequests[0].params.pageTemplate; } @@ -87,6 +91,12 @@ export const spec = { payload.uspConsent = bidderRequest.uspConsent; } + if (_getEids(validBidRequests[0])) { + payload.user = { + eids: _getEids(validBidRequests[0]) + } + } + const payloadObject = JSON.stringify(payload); return { method: 'POST', @@ -136,21 +146,23 @@ export const spec = { */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { let syncurl = ''; + if (!isSynced) { + // Attaching GDPR Consent Params in UserSync url + if (gdprConsent) { + syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (uspConsent && uspConsent.consentString) { + syncurl += `&ccpa_consent=${uspConsent.consentString}`; + } - // Attaching GDPR Consent Params in UserSync url - if (gdprConsent) { - syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); - syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); - } - if (uspConsent && uspConsent.consentString) { - syncurl += `&ccpa_consent=${uspConsent.consentString}`; - } - - if (syncOptions.iframeEnabled) { - return [{ - type: 'iframe', - url: USER_SYNC_URL_IFRAME + syncurl - }]; + if (syncOptions.iframeEnabled) { + isSynced = true; + return [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + syncurl + }]; + } } }, }; @@ -218,4 +230,14 @@ function _getFloor (bid, sizes, currency) { return floor !== null ? floor : bid.params.floor; } +function _getEids(bidRequest) { + if (deepAccess(bidRequest, 'userIdAsEids')) { + return bidRequest.userIdAsEids; + } +} + +export function resetSync() { + isSynced = false; +} + registerBidder(spec); diff --git a/modules/optidigitalBidAdapter.md b/modules/optidigitalBidAdapter.md index 466dfb3bef2..327e7a27c75 100755 --- a/modules/optidigitalBidAdapter.md +++ b/modules/optidigitalBidAdapter.md @@ -37,10 +37,13 @@ Bidder Adapter for Prebid.js. ``` pbjs.setConfig({ - userSync: { - iframeEnabled: true, - syncEnabled: true, - syncDelay: 3000 - } +  userSync: { +    filterSettings: { +      iframe: { +        bidders: '*', // '*' represents all bidders +        filter: 'include' +      } +    } +  } }); ``` diff --git a/modules/optimeraRtdProvider.js b/modules/optimeraRtdProvider.js index dfe8f1bfcf2..04d9b9d1b9f 100644 --- a/modules/optimeraRtdProvider.js +++ b/modules/optimeraRtdProvider.js @@ -16,6 +16,7 @@ * @property {string} clientID * @property {string} optimeraKeyName * @property {string} device + * @property {string} apiVersion */ import { logInfo, logError } from '../src/utils.js'; @@ -38,7 +39,10 @@ export let optimeraKeyName = 'hb_deal_optimera'; * the targeting values. * @type {string} */ -export const scoresBaseURL = 'https://dyv1bugovvq1g.cloudfront.net/'; +export const scoresBaseURL = { + v0: 'https://dyv1bugovvq1g.cloudfront.net/', + v1: 'https://v1.oapi26b.com/', +}; /** * Optimera Score File URL. @@ -58,6 +62,12 @@ export let clientID; */ export let device = 'default'; +/** + * Optional apiVersion parameter. + * @type {string} + */ +export let apiVersion = 'v0'; + /** * Targeting object for all ad positions. * @type {string} @@ -127,6 +137,7 @@ export function onAuctionInit(auctionDetails, config, userConsent) { /** * Initialize the Module. + * moduleConfig.params.apiVersion can be either v0 or v1. */ export function init(moduleConfig) { _moduleParams = moduleConfig.params; @@ -138,6 +149,9 @@ export function init(moduleConfig) { if (_moduleParams.device) { device = _moduleParams.device; } + if (_moduleParams.apiVersion) { + apiVersion = (_moduleParams.apiVersion.includes('v1', 'v0')) ? _moduleParams.apiVersion : 'v0'; + } setScoresURL(); scoreFileRequest(); return true; @@ -162,7 +176,15 @@ export function init(moduleConfig) { export function setScoresURL() { const optimeraHost = window.location.host; const optimeraPathName = window.location.pathname; - const newScoresURL = `${scoresBaseURL}${clientID}/${optimeraHost}${optimeraPathName}.js`; + const baseUrl = scoresBaseURL[apiVersion] ? scoresBaseURL[apiVersion] : scoresBaseURL.v0; + let newScoresURL; + + if (apiVersion === 'v1') { + newScoresURL = `${baseUrl}api/products/scores?c=${clientID}&h=${optimeraHost}&p=${optimeraPathName}&s=${device}`; + } else { + newScoresURL = `${baseUrl}${clientID}/${optimeraHost}${optimeraPathName}.js`; + } + if (scoresURL !== newScoresURL) { scoresURL = newScoresURL; fetchScoreFile = true; diff --git a/modules/optimeraRtdProvider.md b/modules/optimeraRtdProvider.md index 610dec537e0..8b66deb5ad5 100644 --- a/modules/optimeraRtdProvider.md +++ b/modules/optimeraRtdProvider.md @@ -1,6 +1,6 @@ # Overview ``` -Module Name: Optimera Real Time Date Module +Module Name: Optimera Real Time Data Module Module Type: RTD Module Maintainer: mcallari@optimera.nyc ``` @@ -26,7 +26,8 @@ Configuration example for using RTD module with `optimera` provider params: { clientID: '9999', optimeraKeyName: 'optimera', - device: 'de' + device: 'de', + apiVersion: 'v0', } } ] @@ -42,3 +43,4 @@ Contact Optimera to get assistance with the params. | clientID | string | required | Optimera Client ID | | optimeraKeyName | string | optional | GAM key name for Optimera. If migrating from the Optimera bidder adapter this will default to hb_deal_optimera and can be ommitted from the configuration. | | device | string | optional | Device type code for mobile, tablet, or desktop. Either mo, tb, de | +| apiVersion | string | optional | Optimera API Versions. Either v0, or v1. ** Note: v1 wll need to be enabled specifically for your account, otherwise use v0. \ No newline at end of file diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index f135ebb2bd1..efc2effdd62 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -1,11 +1,11 @@ import { isFn, isPlainObject } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {getGlobal} from '../src/prebidGlobal.js'; +import { getGlobal } from '../src/prebidGlobal.js'; -const storageManager = getStorageManager({bidderCode: 'orbidder'}); +const storageManager = getStorageManager({ bidderCode: 'orbidder' }); /** * Determines whether or not the given bid response is valid. @@ -69,7 +69,7 @@ export const spec = { return !!(bid.sizes && bid.bidId && bid.params && (bid.params.accountId && (typeof bid.params.accountId === 'string')) && (bid.params.placementId && (typeof bid.params.placementId === 'string')) && - ((typeof bid.params.profile === 'undefined') || (typeof bid.params.profile === 'object'))); + ((typeof bid.params.keyValues === 'undefined') || (typeof bid.params.keyValues === 'object'))); }, /** @@ -99,15 +99,7 @@ export const spec = { data: { v: getGlobal().version, pageUrl: referer, - bidId: bidRequest.bidId, - auctionId: bidRequest.auctionId, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - transactionId: bidRequest.ortb2Imp?.ext?.tid, - adUnitCode: bidRequest.adUnitCode, - bidRequestCount: bidRequest.bidRequestCount, - params: bidRequest.params, - sizes: bidRequest.sizes, - mediaTypes: bidRequest.mediaTypes + ...bidRequest // get all data provided by bid request } }; diff --git a/modules/orbitsoftBidAdapter.js b/modules/orbitsoftBidAdapter.js index 4c3f2e38c58..f55c7ff9917 100644 --- a/modules/orbitsoftBidAdapter.js +++ b/modules/orbitsoftBidAdapter.js @@ -1,5 +1,6 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getBidIdParameter} from '../src/utils.js'; const BIDDER_CODE = 'orbitsoft'; let styleParamsMap = { @@ -45,10 +46,10 @@ export const spec = { for (let i = 0; i < validBidRequests.length; i++) { bidRequest = validBidRequests[i]; let bidRequestParams = bidRequest.params; - let placementId = utils.getBidIdParameter('placementId', bidRequestParams); - let requestUrl = utils.getBidIdParameter('requestUrl', bidRequestParams); - let referrer = utils.getBidIdParameter('ref', bidRequestParams); - let location = utils.getBidIdParameter('loc', bidRequestParams); + let placementId = getBidIdParameter('placementId', bidRequestParams); + let requestUrl = getBidIdParameter('requestUrl', bidRequestParams); + let referrer = getBidIdParameter('ref', bidRequestParams); + let location = getBidIdParameter('loc', bidRequestParams); // Append location & referrer if (location === '') { location = utils.getWindowLocation(); @@ -58,7 +59,7 @@ export const spec = { } // Styles params - let stylesParams = utils.getBidIdParameter('style', bidRequestParams); + let stylesParams = getBidIdParameter('style', bidRequestParams); let stylesParamsArray = {}; for (let currentValue in stylesParams) { if (stylesParams.hasOwnProperty(currentValue)) { @@ -74,7 +75,7 @@ export const spec = { } } // Custom params - let customParams = utils.getBidIdParameter('customParams', bidRequestParams); + let customParams = getBidIdParameter('customParams', bidRequestParams); let customParamsArray = {}; for (let customField in customParams) { if (customParams.hasOwnProperty(customField)) { diff --git a/modules/otmBidAdapter.js b/modules/otmBidAdapter.js index 6125cee6593..7d4049e3054 100644 --- a/modules/otmBidAdapter.js +++ b/modules/otmBidAdapter.js @@ -2,14 +2,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { logInfo, logError, - getBidIdParameter, _each, getValue, isFn, isPlainObject, isArray, isStr, - isNumber, + isNumber, getBidIdParameter, } from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; diff --git a/modules/outbrainBidAdapter.js b/modules/outbrainBidAdapter.js index 0637d680912..6015ff37e08 100644 --- a/modules/outbrainBidAdapter.js +++ b/modules/outbrainBidAdapter.js @@ -3,6 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; import {OUTSTREAM} from '../src/video.js'; import {_map, deepAccess, deepSetValue, isArray, logWarn, replaceAuctionPrice} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; @@ -23,6 +24,9 @@ const NATIVE_PARAMS = { cta: { id: 1, type: 12, name: 'data' } }; const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; +const OB_USER_TOKEN_KEY = 'OB-USER-TOKEN'; + +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, @@ -130,6 +134,11 @@ export const spec = { request.test = 1; } + const obUserToken = storage.getDataFromLocalStorage(OB_USER_TOKEN_KEY) + if (obUserToken) { + deepSetValue(request, 'user.ext.obusertoken', obUserToken) + } + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies')) { deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString) deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1) @@ -140,6 +149,13 @@ export const spec = { if (config.getConfig('coppa') === true) { deepSetValue(request, 'regs.coppa', config.getConfig('coppa') & 1) } + if (bidderRequest.gppConsent) { + deepSetValue(request, 'regs.ext.gpp', bidderRequest.gppConsent.gppString) + deepSetValue(request, 'regs.ext.gpp_sid', bidderRequest.gppConsent.applicableSections) + } else if (deepAccess(bidderRequest, 'ortb2.regs.gpp')) { + deepSetValue(request, 'regs.ext.gpp', bidderRequest.ortb2.regs.gpp) + deepSetValue(request, 'regs.ext.gpp_sid', bidderRequest.ortb2.regs.gpp_sid) + } if (eids) { deepSetValue(request, 'user.ext.eids', eids); @@ -203,7 +219,7 @@ export const spec = { } }).filter(Boolean); }, - getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent, gppConsent) => { const syncs = []; let syncUrl = config.getConfig('outbrain.usersyncUrl'); @@ -216,6 +232,10 @@ export const spec = { if (uspConsent) { query.push('us_privacy=' + encodeURIComponent(uspConsent)); } + if (gppConsent) { + query.push('gpp=' + encodeURIComponent(gppConsent.gppString)); + query.push('gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(','))); + } syncs.push({ type: 'image', diff --git a/modules/oxxionAnalyticsAdapter.js b/modules/oxxionAnalyticsAdapter.js index c4a51bfb7fa..25732d440ff 100644 --- a/modules/oxxionAnalyticsAdapter.js +++ b/modules/oxxionAnalyticsAdapter.js @@ -21,8 +21,9 @@ let saveEvents = {} let allEvents = {} let auctionEnd = {} let initOptions = {} +let mode = {}; let endpoint = 'https://default' -let requestsAttributes = ['adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'params', 'userId', 'labelAny', 'bids', 'adId']; +let requestsAttributes = ['adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'params', 'userId', 'labelAny', 'bids', 'adId', 'ova']; function getAdapterNameForAlias(aliasName) { return adapterManager.aliasRegistry[aliasName] || aliasName; @@ -41,16 +42,27 @@ function filterAttributes(arg, removead) { } if (typeof arg['gdprConsent'] != 'undefined') { response['gdprConsent'] = {}; - if (typeof arg['gdprConsent']['consentString'] != 'undefined') { response['gdprConsent']['consentString'] = arg['gdprConsent']['consentString']; } + if (typeof arg['gdprConsent']['consentString'] != 'undefined') { + response['gdprConsent']['consentString'] = arg['gdprConsent']['consentString']; + } } - if (typeof arg['meta'] == 'object' && typeof arg['meta']['advertiserDomains'] != 'undefined') { - response['meta'] = {'advertiserDomains': arg['meta']['advertiserDomains']}; + if (typeof arg['meta'] == 'object') { + response['meta'] = {}; + if (typeof arg['meta']['advertiserDomains'] != 'undefined') { + response['meta']['advertiserDomains'] = arg['meta']['advertiserDomains']; + } + if (typeof arg['meta']['demandSource'] == 'string') { + response['meta']['demandSource'] = arg['meta']['demandSource']; + } } requestsAttributes.forEach((attr) => { if (typeof arg[attr] != 'undefined') { response[attr] = arg[attr]; } }); - if (typeof response['creativeId'] == 'number') { response['creativeId'] = response['creativeId'].toString(); } + if (typeof response['creativeId'] == 'number') { + response['creativeId'] = response['creativeId'].toString(); + } } + response['oxxionMode'] = mode; return response; } @@ -77,7 +89,8 @@ function cleanAuctionEnd(args) { } function cleanCreatives(args) { - return filterAttributes(args, false); + let stringArgs = JSON.parse(dereferenceWithoutRenderer(args)); + return filterAttributes(stringArgs, false); } function enhanceMediaType(arg) { @@ -93,7 +106,7 @@ function enhanceMediaType(arg) { function addBidResponse(args) { let eventType = BID_RESPONSE; - let argsCleaned = cleanCreatives(JSON.parse(JSON.stringify(args))); ; + let argsCleaned = cleanCreatives(args); ; if (allEvents[eventType] == undefined) { allEvents[eventType] = [] } allEvents[eventType].push(argsCleaned); } @@ -110,7 +123,9 @@ function addTimeout(args) { if (saveEvents[eventType] == undefined) { saveEvents[eventType] = [] } saveEvents[eventType].push(args); let argsCleaned = []; - let argsDereferenced = JSON.parse(JSON.stringify(args)); + let argsDereferenced = {}; + let stringArgs = JSON.parse(dereferenceWithoutRenderer(args)); + argsDereferenced = stringArgs; argsDereferenced.forEach((attr) => { argsCleaned.push(filterAttributes(JSON.parse(JSON.stringify(attr)), false)); }); @@ -118,17 +133,42 @@ function addTimeout(args) { auctionEnd[eventType].push(argsCleaned); } +export const dereferenceWithoutRenderer = function(args) { + if (args.renderer) { + let tmp = args.renderer; + delete args.renderer; + let stringified = JSON.stringify(args); + args['renderer'] = tmp; + return stringified; + } + if (args.bidsReceived) { + let tmp = {} + for (let key in args.bidsReceived) { + if (args.bidsReceived[key].renderer) { + tmp[key] = args.bidsReceived[key].renderer; + delete args.bidsReceived[key].renderer; + } + } + let stringified = JSON.stringify(args); + for (let key in tmp) { + args.bidsReceived[key].renderer = tmp[key]; + } + return stringified; + } + return JSON.stringify(args); +} + function addAuctionEnd(args) { let eventType = AUCTION_END; if (saveEvents[eventType] == undefined) { saveEvents[eventType] = [] } saveEvents[eventType].push(args); - let argsCleaned = cleanAuctionEnd(JSON.parse(JSON.stringify(args))); + let argsCleaned = cleanAuctionEnd(JSON.parse(dereferenceWithoutRenderer(args))); if (auctionEnd[eventType] == undefined) { auctionEnd[eventType] = [] } auctionEnd[eventType].push(argsCleaned); } function handleBidWon(args) { - args = enhanceMediaType(filterAttributes(JSON.parse(JSON.stringify(args)), true)); + args = enhanceMediaType(filterAttributes(JSON.parse(dereferenceWithoutRenderer(args)), true)); let increment = args['cpm']; if (typeof saveEvents['auctionEnd'] == 'object') { saveEvents['auctionEnd'].forEach((auction) => { @@ -143,6 +183,15 @@ function handleBidWon(args) { } }); } + if (auction['auctionId'] == args['auctionId'] && typeof auction['bidderRequests'] == 'object') { + auction['bidderRequests'].forEach((req) => { + req.bids.forEach((bid) => { + if (bid['bidId'] == args['requestId'] && bid['transactionId'] == args['transactionId']) { + args['ova'] = bid['ova']; + } + }); + }); + } }); } args['cpmIncrement'] = increment; @@ -192,7 +241,8 @@ let oxxionAnalytics = Object.assign(adapter({url, analyticsType}), { addTimeout(args); break; } - }}); + } +}); // save the base class function oxxionAnalytics.originEnableAnalytics = oxxionAnalytics.enableAnalytics; @@ -201,7 +251,12 @@ oxxionAnalytics.originEnableAnalytics = oxxionAnalytics.enableAnalytics; oxxionAnalytics.enableAnalytics = function (config) { oxxionAnalytics.originEnableAnalytics(config); // call the base class function initOptions = config.options; - if (initOptions.domain) { endpoint = 'https://' + initOptions.domain; } + if (initOptions.domain) { + endpoint = 'https://' + initOptions.domain; + } + if (window.OXXION_MODE) { + mode = window.OXXION_MODE; + } }; adapterManager.registerAnalyticsAdapter({ diff --git a/modules/oxxionRtdProvider.js b/modules/oxxionRtdProvider.js index 4b5a7547088..c979a10d00c 100644 --- a/modules/oxxionRtdProvider.js +++ b/modules/oxxionRtdProvider.js @@ -1,97 +1,145 @@ import { submodule } from '../src/hook.js' -import { deepAccess, logInfo } from '../src/utils.js' +import { logInfo, logError } from '../src/utils.js' +import { ajax } from '../src/ajax.js'; +import adapterManager from '../src/adapterManager.js'; -const oxxionRtdSearchFor = [ 'adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'userId', 'labelAny', 'adId' ]; const LOG_PREFIX = 'oxxionRtdProvider submodule: '; -const allAdUnits = []; +const bidderAliasRegistry = adapterManager.aliasRegistry || {}; /** @type {RtdSubmodule} */ export const oxxionSubmodule = { name: 'oxxionRtd', init: init, getBidRequestData: getAdUnits, - onBidResponseEvent: insertVideoTracking, + getRequestsList: getRequestsList, + getFilteredAdUnitsOnBidRates: getFilteredAdUnitsOnBidRates, }; function init(config, userConsent) { - if (!config.params || !config.params.domain || !config.params.contexts || !Array.isArray(config.params.contexts) || config.params.contexts.length == 0) { - return false - } - return true; + if (!config.params || !config.params.domain) { return false } + if (typeof config.params.threshold != 'undefined' && typeof config.params.samplingRate == 'number') { return true } + return false; } function getAdUnits(reqBidsConfigObj, callback, config, userConsent) { - const reqAdUnits = reqBidsConfigObj.adUnits; - if (Array.isArray(reqAdUnits)) { - reqAdUnits.forEach(adunit => { - if (config.params.contexts.includes(deepAccess(adunit, 'mediaTypes.video.context'))) { - allAdUnits.push(adunit); + logInfo(LOG_PREFIX + 'started with ', config); + if (typeof config.params.threshold != 'undefined' && typeof config.params.samplingRate == 'number') { + let filteredBids; + const requests = getRequestsList(reqBidsConfigObj); + const gdpr = userConsent && userConsent.gdpr ? userConsent.gdpr.consentString : null; + const payload = { + gdpr, + requests + }; + const endpoint = 'https://' + config.params.domain + '.oxxion.io/analytics/bid_rate_interests'; + getPromisifiedAjax(endpoint, JSON.stringify(payload), { + method: 'POST', + withCredentials: true + }).then(bidsRateInterests => { + if (bidsRateInterests.length) { + [reqBidsConfigObj.adUnits, filteredBids] = getFilteredAdUnitsOnBidRates(bidsRateInterests, reqBidsConfigObj.adUnits, config.params, true); } - }); + if (filteredBids.length > 0) { + getPromisifiedAjax('https://' + config.params.domain + '.oxxion.io/analytics/request_rejecteds', JSON.stringify({'bids': filteredBids, 'gdpr': gdpr}), { + method: 'POST', + withCredentials: true + }); + } + if (typeof callback == 'function') { callback(); } + }).catch(error => logError(LOG_PREFIX, 'bidInterestError', error)); } } -function insertVideoTracking(bidResponse, config, userConsent) { - // this should only be do for video bids - if (bidResponse.mediaType === 'video') { - let maxCpm = 0; - const trackingUrl = getImpUrl(config, bidResponse, maxCpm); - if (!trackingUrl) { - return; - } - // Vast Impression URL - if (bidResponse.vastUrl) { - bidResponse.vastImpUrl = bidResponse.vastImpUrl - ? trackingUrl + '&url=' + encodeURI(bidResponse.vastImpUrl) - : trackingUrl; - logInfo(LOG_PREFIX + 'insert into vastImpUrl for adId ' + bidResponse.adId); - } - // Vast XML document - if (bidResponse.vastXml !== undefined) { - const doc = new DOMParser().parseFromString(bidResponse.vastXml, 'text/xml'); - const wrappers = doc.querySelectorAll('VAST Ad Wrapper, VAST Ad InLine'); - let hasAltered = false; - if (wrappers.length) { - wrappers.forEach(wrapper => { - const impression = doc.createElement('Impression'); - impression.appendChild(doc.createCDATASection(trackingUrl)); - wrapper.appendChild(impression) - }); - bidResponse.vastXml = new XMLSerializer().serializeToString(doc); - hasAltered = true; +function getPromisifiedAjax (url, data = {}, options = {}) { + return new Promise((resolve, reject) => { + const callbacks = { + success(responseText, { response }) { + resolve(JSON.parse(response)); + }, + error(error) { + reject(error); } - if (hasAltered) { - logInfo(LOG_PREFIX + 'insert into vastXml for adId ' + bidResponse.adId); - } - } - } + }; + ajax(url, callbacks, data, options); + }) } -function getImpUrl(config, data, maxCpm) { - const adUnitCode = data.adUnitCode; - const adUnits = allAdUnits.find(adunit => adunit.code === adUnitCode && - 'mediaTypes' in adunit && - 'video' in adunit.mediaTypes && - typeof adunit.mediaTypes.video.context === 'string'); - const context = adUnits !== undefined - ? adUnits.mediaTypes.video.context - : 'unknown'; - if (!config.params.contexts.includes(context)) { - return false; - } - let trackingImpUrl = 'https://' + config.params.domain + '.oxxion.io/analytics/vast_imp?'; - trackingImpUrl += oxxionRtdSearchFor.reduce((acc, param) => { - switch (typeof data[param]) { - case 'string': - case 'number': - acc += param + '=' + data[param] + '&' - break; +function getFilteredAdUnitsOnBidRates (bidsRateInterests, adUnits, params, useSampling) { + const { threshold, samplingRate } = params; + const sampling = getRandomNumber(100) < samplingRate && useSampling; + const filteredBids = []; + // Separate bidsRateInterests in two groups against threshold & samplingRate + const { interestingBidsRates, uninterestingBidsRates, sampledBidsRates } = bidsRateInterests.reduce((acc, interestingBid) => { + const isBidRateUpper = typeof threshold == 'number' ? interestingBid.rate === true || interestingBid.rate > threshold : interestingBid.suggestion; + const isBidInteresting = isBidRateUpper || sampling; + const key = isBidInteresting ? 'interestingBidsRates' : 'uninterestingBidsRates'; + acc[key].push(interestingBid); + if (!isBidRateUpper && sampling) { + acc['sampledBidsRates'].push(interestingBid); } return acc; - }, ''); - const cpmIncrement = 0.0; - return trackingImpUrl + 'cpmIncrement=' + cpmIncrement + '&context=' + context; + }, { + interestingBidsRates: [], + uninterestingBidsRates: [], // Do something with later + sampledBidsRates: [] + }); + logInfo(LOG_PREFIX, 'getFilteredAdUnitsOnBidRates()', interestingBidsRates, uninterestingBidsRates); + // Filter bids and adUnits against interesting bids rates + const newAdUnits = adUnits.filter(({ bids = [] }, adUnitIndex) => { + adUnits[adUnitIndex].bids = bids.filter((bid, bidIndex) => { + if (!params.bidders || params.bidders.includes(bid.bidder)) { + const index = interestingBidsRates.findIndex(({ id }) => id === bid._id); + if (index == -1) { + let tmpBid = bid; + tmpBid['code'] = adUnits[adUnitIndex].code; + tmpBid['mediaTypes'] = adUnits[adUnitIndex].mediaTypes; + tmpBid['originalBidder'] = bidderAliasRegistry[bid.bidder] || bid.bidder; + if (tmpBid.floorData) { + delete tmpBid.floorData; + } + filteredBids.push(tmpBid); + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'filtered'; + } else { + if (sampledBidsRates.findIndex(({ id }) => id === bid._id) == -1) { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'cleared'; + } else { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'sampled'; + logInfo(LOG_PREFIX + ' sampled ! '); + } + } + delete bid._id; + return index !== -1; + } else { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'protected'; + return true; + } + }); + return !!adUnits[adUnitIndex].bids.length; + }); + return [newAdUnits, filteredBids]; +} + +function getRandomNumber (max = 10) { + return Math.round(Math.random() * max); +} + +function getRequestsList(reqBidsConfigObj) { + let count = 0; + return reqBidsConfigObj.adUnits.flatMap(({ + bids = [], + mediaTypes = {}, + code = '' + }) => bids.reduce((acc, { bidder = '', params = {} }, index) => { + const id = count++; + bids[index]._id = id; + return acc.concat({ + id, + adUnit: code, + bidder, + mediaTypes, + }); + }, [])); } submodule('realTimeData', oxxionSubmodule); diff --git a/modules/oxxionRtdProvider.md b/modules/oxxionRtdProvider.md index 061acdbe11a..bfdbfae1fa9 100644 --- a/modules/oxxionRtdProvider.md +++ b/modules/oxxionRtdProvider.md @@ -7,12 +7,12 @@ Maintainer: tech@oxxion.io # Oxxion Real-Time-Data submodule Oxxion helps you to understand how your prebid stack performs. -This Rtd module is to use in order to improve video events tracking. +This Rtd module purpose is to filter bidders requested. # Integration Make sure to have the following modules listed while building prebid : `rtdModule,oxxionRtdProvider` -`rtbModule` is required to activate real-time-data submodules. +`rtdModule` is required to activate real-time-data submodules. For example : ``` gulp build --modules=schain,priceFloors,currency,consentManagement,appnexusBidAdapter,rubiconBidAdapter,rtdModule,oxxionRtdProvider @@ -23,14 +23,15 @@ Then add the oxxion Rtd module to your prebid configuration : pbjs.setConfig( ... realTimeData: { - auctionDelay: 200, + auctionDelay: 300, dataProviders: [ { name: "oxxionRtd", waitForIt: true, params: { domain: "test.endpoint", - contexts: ["instream"], + threshold: false, + samplingRate: 10, } } ] @@ -39,10 +40,17 @@ pbjs.setConfig( ) ``` -# setConfig Parameters +# setConfig Parameters General | Name | Type | Description | |:---------------------------------|:---------|:------------------------------------------------------------------------------------------------------------| | domain | String | This string identifies yourself in Oxxion's systems and is provided to you by your Oxxion representative. | -| contexts | Array | Array defining which video contexts to add tracking events into. Values can be instream and/or outstream. | + +# setConfig Parameters for bidder filtering + +| Name | Type | Description | +|:---------------------------------|:-----------|:------------------------------------------------------------------------------------------------------------| +| threshold | Float/Bool | False or minimum expected bid rate to call a bidder (ex: 1.0 for 1% bid rate). | +| samplingRate | Integer | Percentage of request not meeting the criterias to run anyway in order to check for any change. | +| bidders | Array | Optional: If set, filtering will only be applied to bidders listed. diff --git a/modules/ozoneBidAdapter.js b/modules/ozoneBidAdapter.js index 1b725ce3a05..0d921f57cda 100644 --- a/modules/ozoneBidAdapter.js +++ b/modules/ozoneBidAdapter.js @@ -7,7 +7,8 @@ import { isArray, contains, mergeDeep, - parseUrl + parseUrl, + generateUUID } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; @@ -16,15 +17,15 @@ import {getPriceBucketString} from '../src/cpmBucketManager.js'; import { Renderer } from '../src/Renderer.js'; import {getRefererInfo} from '../src/refererDetection.js'; const BIDDER_CODE = 'ozone'; -const ORIGIN = 'https://elb.the-ozone-project.com'; // applies only to auction & cookie +const ORIGIN = 'https://elb.the-ozone-project.com' // applies only to auction & cookie const AUCTIONURI = '/openrtb2/auction'; const OZONECOOKIESYNC = '/static/load-cookie.html'; const OZONE_RENDERER_URL = 'https://prebid.the-ozone-project.com/ozone-renderer.js'; const ORIGIN_DEV = 'https://test.ozpr.net'; -const OZONEVERSION = '2.8.0'; +const OZONEVERSION = '2.9.1'; export const spec = { gvlid: 524, - aliases: [{code: 'lmc', gvlid: 524}], + aliases: [{code: 'lmc', gvlid: 524}, {code: 'venatus', gvlid: 524}], version: OZONEVERSION, code: BIDDER_CODE, supportedMediaTypes: [VIDEO, BANNER], @@ -36,7 +37,8 @@ export const spec = { 'keyPrefix': 'oz', 'auctionUrl': ORIGIN + AUCTIONURI, 'cookieSyncUrl': ORIGIN + OZONECOOKIESYNC, - 'rendererUrl': OZONE_RENDERER_URL + 'rendererUrl': OZONE_RENDERER_URL, + 'batchRequests': false /* you can change this to true OR override it in the config: config.ozone.batchRequests */ }, loadWhitelabelData(bid) { if (this.propertyBag.whitelabel) { return; } @@ -73,6 +75,12 @@ export const spec = { this.propertyBag.whitelabel.auctionUrl = bidderConfig.endpointOverride.auctionUrl; } } + if (bidderConfig.hasOwnProperty('batchRequests')) { + this.propertyBag.whitelabel.batchRequests = bidderConfig.batchRequests; + } + if (arr.hasOwnProperty('batchRequests')) { + this.propertyBag.whitelabel.batchRequests = true; + } try { if (arr.hasOwnProperty('auction') && arr.auction === 'dev') { logInfo('GET: auction=dev'); @@ -94,11 +102,15 @@ export const spec = { getRendererUrl() { return this.propertyBag.whitelabel.rendererUrl; }, + isBatchRequests() { + logInfo('isBatchRequests going to return ', this.propertyBag.whitelabel.batchRequests); + return this.propertyBag.whitelabel.batchRequests; + }, isBidRequestValid(bid) { this.loadWhitelabelData(bid); logInfo('isBidRequestValid : ', config.getConfig(), bid); let adUnitCode = bid.adUnitCode; // adunit[n].code - let err1 = 'VALIDATION FAILED : missing {param} : siteId, placementId and publisherId are REQUIRED'; + let err1 = 'VALIDATION FAILED : missing {param} : siteId, placementId and publisherId are REQUIRED' if (!(bid.params.hasOwnProperty('placementId'))) { logError(err1.replace('{param}', 'placementId'), adUnitCode); return false; @@ -199,7 +211,7 @@ export const spec = { let placementId = placementIdOverrideFromGetParam || this.getPlacementId(ozoneBidRequest); // prefer to use a valid override param, else the bidRequest placement Id obj.id = ozoneBidRequest.bidId; // this causes an error if we change it to something else, even if you update the bidRequest object: "WARNING: Bidder ozone made bid for unknown request ID: mb7953.859498327448. Ignoring." obj.tagid = placementId; - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().page); obj.secure = parsed.protocol === 'https' ? 1 : 0; let arrBannerSizes = []; if (!ozoneBidRequest.hasOwnProperty('mediaTypes')) { @@ -262,9 +274,7 @@ export const spec = { obj.placementId = placementId; deepSetValue(obj, 'ext.prebid', {'storedrequest': {'id': placementId}}); obj.ext[whitelabelBidder] = {}; - // TODO: fix auctionId/transactionID leak: https://github.com/prebid/Prebid.js/issues/9781 obj.ext[whitelabelBidder].adUnitCode = ozoneBidRequest.adUnitCode; // eg. 'mpu' - obj.ext[whitelabelBidder].transactionId = ozoneBidRequest.transactionId; // this is the transactionId PER adUnit, common across bidders for this unit if (ozoneBidRequest.params.hasOwnProperty('customData')) { obj.ext[whitelabelBidder].customData = ozoneBidRequest.params.customData; } @@ -291,6 +301,10 @@ export const spec = { if (!schain && deepAccess(ozoneBidRequest, 'schain')) { schain = ozoneBidRequest.schain; } + let gpid = deepAccess(ozoneBidRequest, 'ortb2Imp.ext.gpid'); + if (gpid) { + deepSetValue(obj, 'ext.gpid', gpid); + } return obj; }); let extObj = {}; @@ -326,7 +340,7 @@ export const spec = { let userExtEids = deepAccess(validBidRequests, '0.userIdAsEids', []); // generate the UserIDs in the correct format for UserId module ozoneRequest.site = { 'publisher': {'id': htmlParams.publisherId}, - 'page': getRefererInfo().page, + 'page': this.getRefererInfo().page, 'id': htmlParams.siteId }; ozoneRequest.test = config.getConfig('debug') ? 1 : 0; @@ -355,13 +369,33 @@ export const spec = { if (config.getConfig('coppa') === true) { deepSetValue(ozoneRequest, 'regs.coppa', 1); } + let ozUuid = generateUUID(); + if (this.isBatchRequests()) { + logInfo('going to batch the requests'); + let arrRet = []; // return an array of objects containing data describing max 10 bids + for (let i = 0; i < tosendtags.length; i += 10) { + ozoneRequest.id = ozUuid; // Unique ID of the bid request, provided by the exchange. (REQUIRED) + ozoneRequest.imp = tosendtags.slice(i, i + 10); + ozoneRequest.ext = extObj; + deepSetValue(ozoneRequest, 'user.ext.eids', userExtEids); + if (ozoneRequest.imp.length > 0) { + arrRet.push({ + method: 'POST', + url: this.getAuctionUrl(), + data: JSON.stringify(ozoneRequest), + bidderRequest: bidderRequest + }); + } + } + logInfo('batch request going to return : ', arrRet); + return arrRet; + } + logInfo('requests will not be batched.'); if (singleRequest) { logInfo('buildRequests starting to generate response for a single request'); - ozoneRequest.id = bidderRequest.auctionId; // Unique ID of the bid request, provided by the exchange. - ozoneRequest.auctionId = bidderRequest.auctionId; // not sure if this should be here? + ozoneRequest.id = ozUuid; // Unique ID of the bid request, provided by the exchange. (REQUIRED) ozoneRequest.imp = tosendtags; ozoneRequest.ext = extObj; - deepSetValue(ozoneRequest, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(ozoneRequest, 'user.ext.eids', userExtEids); var ret = { method: 'POST', @@ -377,12 +411,9 @@ export const spec = { let arrRet = tosendtags.map(imp => { logInfo('buildRequests starting to generate non-single response, working on imp : ', imp); let ozoneRequestSingle = Object.assign({}, ozoneRequest); - imp.ext[whitelabelBidder].pageAuctionId = bidderRequest['auctionId']; // make a note in the ext object of what the original auctionId was, in the bidderRequest object - ozoneRequestSingle.id = imp.ext[whitelabelBidder].transactionId; // Unique ID of the bid request, provided by the exchange. - ozoneRequestSingle.auctionId = imp.ext[whitelabelBidder].transactionId; // not sure if this should be here? + ozoneRequestSingle.id = generateUUID(); // Unique ID of the bid request, provided by the exchange. (REQUIRED) ozoneRequestSingle.imp = [imp]; ozoneRequestSingle.ext = extObj; - deepSetValue(ozoneRequestSingle, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(ozoneRequestSingle, 'user.ext.eids', userExtEids); logInfo('buildRequests RequestSingle (for non-single) = ', ozoneRequestSingle); return { @@ -424,6 +455,7 @@ export const spec = { logInfo(`interpretResponse time: ${startTime} . Time between buildRequests done and interpretResponse start was ${startTime - this.propertyBag.buildRequestsEnd}ms`); logInfo(`serverResponse, request`, JSON.parse(JSON.stringify(serverResponse)), JSON.parse(JSON.stringify(request))); serverResponse = serverResponse.body || {}; + let aucId = serverResponse.id; // this will be correct for single requests and non-single if (!serverResponse.hasOwnProperty('seatbid')) { return []; } @@ -518,7 +550,7 @@ export const spec = { } } let {seat: winningSeat, bid: winningBid} = ozoneGetWinnerForRequestBid(thisBid.bidId, serverResponse.seatbid); - adserverTargeting[whitelabelPrefix + '_auc_id'] = String(request.bidderRequest.auctionId); + adserverTargeting[whitelabelPrefix + '_auc_id'] = String(aucId); // was request.bidderRequest.auctionId adserverTargeting[whitelabelPrefix + '_winner'] = String(winningSeat); adserverTargeting[whitelabelPrefix + '_bid'] = 'true'; adserverTargeting[whitelabelPrefix + '_cache_id'] = deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_id', 'no-id'); @@ -686,10 +718,29 @@ export const spec = { return null; }, getGetParametersAsObject() { - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().location); logInfo('getGetParametersAsObject found:', parsed.search); return parsed.search; }, + getRefererInfo() { + if (getRefererInfo().hasOwnProperty('location')) { + logInfo('FOUND location on getRefererInfo OK (prebid >= 7); will use getRefererInfo for location & page'); + return getRefererInfo(); + } else { + logInfo('DID NOT FIND location on getRefererInfo (prebid < 7); will use legacy code that ALWAYS worked reliably to get location & page ;-)'); + try { + return { + page: top.location.href, + location: top.location.href + }; + } catch (e) { + return { + page: window.location.href, + location: window.location.href + }; + } + } + }, blockTheRequest() { let ozRequest = this.getWhitelabelConfigItem('ozone.oz_request'); if (typeof ozRequest == 'boolean' && !ozRequest) { diff --git a/modules/ozoneBidAdapter.md b/modules/ozoneBidAdapter.md index 6f4cf752f22..9787e069283 100644 --- a/modules/ozoneBidAdapter.md +++ b/modules/ozoneBidAdapter.md @@ -73,6 +73,8 @@ adUnits = [{ }]; ``` + +``` //Instream Video adUnit adUnits = [{ @@ -108,4 +110,4 @@ adUnits = [{ } }] }; -``` +``` \ No newline at end of file diff --git a/modules/pairIdSystem.js b/modules/pairIdSystem.js index f90e25fdd0f..489b97d02e3 100644 --- a/modules/pairIdSystem.js +++ b/modules/pairIdSystem.js @@ -78,6 +78,12 @@ export const pairIdSubmodule = { return undefined; } return {'id': ids}; + }, + eids: { + 'pairId': { + source: 'google.com', + atype: 571187 + }, } }; diff --git a/modules/pangleBidAdapter.js b/modules/pangleBidAdapter.js new file mode 100644 index 00000000000..e75a96048b4 --- /dev/null +++ b/modules/pangleBidAdapter.js @@ -0,0 +1,165 @@ +// ver V1.0.4 +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepSetValue, generateUUID, timestamp, deepAccess } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; + +import { Renderer } from '../src/Renderer.js'; + +const BIDDER_CODE = 'pangle'; +const ENDPOINT = 'https://pangle.pangleglobal.com/api/ad/union/web_js/common/get_ads'; + +const OUTSTREAM_RENDERER_URL = 'https://sf16-static.i18n-pglstatp.com/obj/ad-pattern-sg/pangle/web/ads/video.js'; + +const DEFAULT_BID_TTL = 30; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; +const PANGLE_COOKIE = '_pangle_id'; +const COOKIE_EXP = 86400 * 1000 * 365 * 1; // 1 year +export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: BIDDER_CODE }) + +export function isValidUuid(uuid) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + uuid + ); +} + +function getPangleCookieId() { + let sid = storage.cookiesAreEnabled() && storage.getCookie(PANGLE_COOKIE); + + if (!sid || !isValidUuid(sid)) { + sid = generateUUID(); + setPangleCookieId(sid); + } + + return sid; +} + +function setPangleCookieId(sid) { + if (storage.cookiesAreEnabled()) { + const expires = new Date(timestamp() + COOKIE_EXP).toGMTString(); + + storage.setCookie(PANGLE_COOKIE, sid, expires); + } +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + const data = converter.toORTB({ + bidRequests, + bidderRequest, + context: { mediaType }, + }); + const devicetype = spec.getDeviceType(navigator.userAgent); + deepSetValue(data, 'device.devicetype', devicetype); + if (bidderRequest.userId && typeof bidderRequest.userId === 'object') { + const pangleId = getPangleCookieId(); + // add pangle cookie + const _eids = data.user?.ext?.eids ?? []; + deepSetValue(data, 'user.ext.eids', [ + ..._eids, + { + source: document.location.host, + uids: [ + { + id: pangleId, + atype: 1, + }, + ], + }, + ]); + } + bidRequests.forEach((item, idx) => { + deepSetValue(data.imp[idx], 'ext.networkids', item.params); + deepSetValue(data.imp[idx], 'banner.api', [5]); + deepSetValue(data, 'test', item.params.test ?? 0) + }); + return { + method: 'POST', + url: ENDPOINT, + data, + options: { contentType: 'application/json', withCredentials: true } + } +} + +function isVideoBid(bid) { + return !!deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return !!deepAccess(bid, 'mediaTypes.banner'); +} + +function renderOutstream(bid) { + bid.renderer.push(() => { + window.outstreamPlayer({ bid, codeId: bid.adUnitCode }); + }); +} + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY, + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + const { bidRequest } = context; + if (bidRequest.mediaTypes.video?.context === 'outstream') { + const renderer = Renderer.install({id: bid.bidId, url: OUTSTREAM_RENDERER_URL, adUnitCode: bid.adUnitCode}); + renderer.setRender(renderOutstream); + bidResponse.renderer = renderer; + } + return bidResponse; + }, +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + getDeviceType: function (ua) { + if ( + /ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test( + ua.toLowerCase() + ) + ) { + return 5; // 'tablet' + } + if ( + /iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test( + ua.toLowerCase() + ) + ) { + return 4; // 'mobile' + } + return 2; // 'desktop' + }, + + isBidRequestValid: function (bid) { + return Boolean(bid.params.token); + }, + + buildRequests(bidRequests, bidderRequest) { + const videoBids = bidRequests.filter((bid) => isVideoBid(bid)); + const bannerBids = bidRequests.filter((bid) => isBannerBid(bid)); + let requests = bannerBids.length + ? [createRequest(bannerBids, bidderRequest, BANNER)] + : []; + videoBids.forEach((bid) => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + return requests; + }, + + interpretResponse(response, request) { + const bids = converter.fromORTB({ + response: response.body, + request: request.data, + }).bids; + return bids; + }, +}; + +registerBidder(spec); diff --git a/modules/pangleBidAdapter.md b/modules/pangleBidAdapter.md new file mode 100644 index 00000000000..8fc628dcc89 --- /dev/null +++ b/modules/pangleBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +Module Name: pangle Bidder Adapter +Module Type: Bidder Adapter +Maintainer: + +# Description + +An adapter to get a bid from pangle DSP. + +# Test Parameters + +```javascript +var adUnits = [{ + // banner + code: 'test1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + + bids: [{ + bidder: 'pangle', + params: { + token: 'aass', + appid: 612, + placementid: 123, + } + }] +}]; +``` diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index 01ffec3c249..3e3488f72f3 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -7,7 +7,17 @@ // ci trigger: 1 -import {contains, deepClone, inIframe, isEmpty, isPlainObject, logError, logWarn, timestamp} from '../src/utils.js'; +import { + contains, + deepClone, + inIframe, + isEmpty, + isPlainObject, + logError, + logWarn, + pick, + timestamp +} from '../src/utils.js'; import {find} from '../src/polyfill.js'; import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; @@ -368,7 +378,33 @@ export const parrableIdSubmodule = { getId(config, gdprConsentData, currentStoredId) { const configParams = (config && config.params) || {}; return fetchId(configParams, gdprConsentData); - } + }, + eids: { + 'parrableId': { + source: 'parrable.com', + atype: 1, + getValue: function(parrableId) { + if (parrableId.eid) { + return parrableId.eid; + } + if (parrableId.ccpaOptout) { + // If the EID was suppressed due to a non consenting ccpa optout then + // we still wish to provide this as a reason to the adapters + return ''; + } + return null; + }, + getUidExt: function(parrableId) { + const extendedData = pick(parrableId, [ + 'ibaOptout', + 'ccpaOptout' + ]); + if (Object.keys(extendedData).length) { + return extendedData; + } + } + }, + }, }; submodule('userId', parrableIdSubmodule); diff --git a/modules/pgamsspBidAdapter.js b/modules/pgamsspBidAdapter.js new file mode 100644 index 00000000000..7d285daf3c6 --- /dev/null +++ b/modules/pgamsspBidAdapter.js @@ -0,0 +1,212 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'pgamssp'; +const AD_URL = 'https://us-east.pgammedia.com/pbjs'; +const SYNC_URL = 'https://cs.pgammedia.com'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/pgamsspBidAdapter.md b/modules/pgamsspBidAdapter.md new file mode 100644 index 00000000000..c162ec33053 --- /dev/null +++ b/modules/pgamsspBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: PGAMSSP Bidder Adapter +Module Type: PGAMSSP Bidder Adapter +Maintainer: info@pgammedia.com +``` + +# Description + +Connects to PGAMSSP exchange for bids. +PGAMSSP bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/pixfutureBidAdapter.js b/modules/pixfutureBidAdapter.js index 608ba20aa5f..1c3f9b8da1a 100644 --- a/modules/pixfutureBidAdapter.js +++ b/modules/pixfutureBidAdapter.js @@ -3,10 +3,11 @@ import {getStorageManager} from '../src/storageManager.js'; import {BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find, includes} from '../src/polyfill.js'; -import {convertCamelToUnderscore, deepAccess, isArray, isFn, isNumber, isPlainObject} from '../src/utils.js'; +import {deepAccess, isArray, isFn, isNumber, isPlainObject} from '../src/utils.js'; import {auctionManager} from '../src/auctionManager.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import {getANKeywordParam} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; const SOURCE = 'pbjs'; const storageManager = getStorageManager({bidderCode: 'pixfuture'}); diff --git a/modules/prebidServerBidAdapter/config.js b/modules/prebidServerBidAdapter/config.js index f6b8ac9f86a..87274504f64 100644 --- a/modules/prebidServerBidAdapter/config.js +++ b/modules/prebidServerBidAdapter/config.js @@ -1,11 +1,11 @@ // accountId and bidders params are not included here, should be configured by end-user export const S2S_VENDORS = { - 'appnexus': { + 'appnexuspsp': { adapter: 'prebidServer', enabled: true, endpoint: { - p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction' + p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', + noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' }, syncEndpoint: { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', @@ -13,15 +13,6 @@ export const S2S_VENDORS = { }, timeout: 1000 }, - 'appnexuspsp': { - adapter: 'prebidServer', - enabled: true, - endpoint: { - p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', - noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' - }, - timeout: 1000 - }, 'rubicon': { adapter: 'prebidServer', enabled: true, @@ -47,5 +38,14 @@ export const S2S_VENDORS = { noP1Consent: 'https://prebid.openx.net/cookie_sync' }, timeout: 1000 + }, + 'openwrap': { + adapter: 'prebidServer', + enabled: true, + endpoint: { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + timeout: 500 } } diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 3cc38923d57..a530c415839 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -1,6 +1,6 @@ import Adapter from '../../src/adapter.js'; import { - bind, + deepAccess, deepClone, flatten, generateUUID, @@ -15,10 +15,9 @@ import { logWarn, triggerPixel, uniques, - deepAccess, } from '../../src/utils.js'; import CONSTANTS from '../../src/constants.json'; -import adapterManager from '../../src/adapterManager.js'; +import adapterManager, {s2sActivityParams} from '../../src/adapterManager.js'; import {config} from '../../src/config.js'; import {addComponentAuction, isValid} from '../../src/adapters/bidderFactory.js'; import * as events from '../../src/events.js'; @@ -29,6 +28,8 @@ import {hook} from '../../src/hook.js'; import {hasPurpose1Consent} from '../../src/utils/gpdr.js'; import {buildPBSRequest, interpretPBSResponse} from './ortbConverter.js'; import {useMetrics} from '../../src/utils/perfMetrics.js'; +import {isActivityAllowed} from '../../src/activities/rules.js'; +import {ACTIVITY_TRANSMIT_UFPD} from '../../src/activities/activities.js'; const getConfig = config.getConfig; @@ -295,7 +296,7 @@ function doAllSyncs(bidders, s2sConfig) { // if PBS reports this bidder doesn't have an ID, then call the sync and recurse to the next sync entry if (thisSync.no_cookie) { - doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, bind.call(doAllSyncs, null, bidders, s2sConfig), s2sConfig); + doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, doAllSyncs.bind(null, bidders, s2sConfig), s2sConfig); } else { // bidder already has an ID, so just recurse to the next sync entry doAllSyncs(bidders, s2sConfig); @@ -354,8 +355,7 @@ function doClientSideSyncs(bidders, gdprConsent, uspConsent, gppConsent) { if (clientAdapter && clientAdapter.registerSyncs) { config.runWithBidder( bidder, - bind.call( - clientAdapter.registerSyncs, + clientAdapter.registerSyncs.bind( clientAdapter, [], gdprConsent, @@ -481,7 +481,11 @@ export function PrebidServer() { done(); doClientSideSyncs(requestedBidders, gdprConsent, uspConsent, gppConsent); }, - onError: done, + onError(msg, error) { + logError(`Prebid server call failed: '${msg}'`, error); + bidRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_ERROR, {error, bidderRequest})); + done(); + }, onBid: function ({adUnit, bid}) { const metrics = bid.metrics = s2sBidRequest.metrics.fork().renameWith(); metrics.checkpoint('addBidResponse'); @@ -499,8 +503,8 @@ export function PrebidServer() { } } }, - onFledge: ({adUnitCode, config}) => { - addComponentAuction(adUnitCode, config); + onFledge: (params) => { + addComponentAuction({auctionId: bidRequests[0].auctionId, ...params}, params.config); } }) } @@ -571,7 +575,11 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques } }, requestJson, - {contentType: 'text/plain', withCredentials: true} + { + contentType: 'text/plain', + withCredentials: true, + browsingTopics: isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, s2sActivityParams(s2sBidRequest.s2sConfig)) + } ); } else { logError('PBS request not made. Check endpoints.'); diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index 9d29b66cfc8..7aeb4302280 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -17,13 +17,14 @@ import {pbsExtensions} from '../../libraries/pbsExtensions/pbsExtensions.js'; import {setImpBidParams} from '../../libraries/pbsExtensions/processors/params.js'; import {SUPPORTED_MEDIA_TYPES} from '../../libraries/pbsExtensions/processors/mediaType.js'; import {IMP, REQUEST, RESPONSE} from '../../src/pbjsORTB.js'; -import {beConvertCurrency} from '../../src/utils/currency.js'; import {redactor} from '../../src/activities/redactor.js'; import {s2sActivityParams} from '../../src/adapterManager.js'; import {activityParams} from '../../src/activities/activityParams.js'; import {MODULE_TYPE_BIDDER} from '../../src/activities/modules.js'; import {isActivityAllowed} from '../../src/activities/rules.js'; import {ACTIVITY_TRANSMIT_TID} from '../../src/activities/activities.js'; +import {currencyCompare} from '../../libraries/currencyUtils/currency.js'; +import {minimum} from '../../src/utils/reducers.js'; const DEFAULT_S2S_TTL = 60; const DEFAULT_S2S_CURRENCY = 'USD'; @@ -141,6 +142,7 @@ const PBS_CONVERTER = ortbConverter({ bidfloor(orig, imp, proxyBidRequest, context) { // for bid floors, we pass each bidRequest associated with this imp through normal bidfloor processing, // and aggregate all of them into a single, minimum floor to put in the request + const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur])); let min; for (const req of context.actualBidRequests.values()) { const floor = {}; @@ -149,14 +151,8 @@ const PBS_CONVERTER = ortbConverter({ if (floor.bidfloorcur == null || floor.bidfloor == null) { min = null; break; - } else if (min == null) { - min = floor; - } else { - const value = beConvertCurrency(floor.bidfloor, floor.bidfloorcur, min.bidfloorcur); - if (value != null && value < min.bidfloor) { - min = floor; - } } + min = min == null ? floor : getMin(min, floor); } if (min != null) { Object.assign(imp, min); @@ -168,6 +164,10 @@ const PBS_CONVERTER = ortbConverter({ // FPD is handled different for PBS - the base request will only contain global FPD; // bidder-specific values are set in ext.prebid.bidderconfig + if (context.transmitTids) { + deepSetValue(ortbRequest, 'source.tid', proxyBidderRequest.auctionId); + } + mergeDeep(ortbRequest, context.s2sBidRequest.ortb2Fragments?.global); // also merge in s2sConfig.extPrebid @@ -240,7 +240,16 @@ const PBS_CONVERTER = ortbConverter({ }, fledgeAuctionConfigs(orig, response, ortbResponse, context) { const configs = Object.values(context.impContext) - .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({adUnitCode: impCtx.adUnit.code, config: cfg.config}))); + .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => { + const bidderReq = impCtx.actualBidderRequests.find(br => br.bidderCode === cfg.bidder); + const bidReq = impCtx.actualBidRequests.get(cfg.bidder); + return { + adUnitCode: impCtx.adUnit.code, + ortb2: bidderReq?.ortb2, + ortb2Imp: bidReq?.ortb2Imp, + config: cfg.config + }; + })); if (configs.length > 0) { response.fledgeAuctionConfigs = configs; } diff --git a/modules/precisoBidAdapter.js b/modules/precisoBidAdapter.js new file mode 100644 index 00000000000..9125f6f3911 --- /dev/null +++ b/modules/precisoBidAdapter.js @@ -0,0 +1,212 @@ +import { logMessage, isFn, deepAccess, logInfo } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +const BIDDER_CODE = 'preciso'; +const AD_URL = 'https://ssp-bidder.mndtrk.com/bid_request/openrtb'; +const URL_SYNC = 'https://ck.2trk.info/rtb/user/usersync.aspx?'; +const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; +const GVLID = 874; +let userId = 'NA'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + gvlid: GVLID, + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(bid.params.publisherId) && bid.params.host == 'prebid'); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + // userId = validBidRequests[0].userId.pubcid; + let winTop = window; + let location; + var offset = new Date().getTimezoneOffset(); + logInfo('timezone ' + offset); + var city = Intl.DateTimeFormat().resolvedOptions().timeZone; + logInfo('location test' + city) + + const countryCode = getCountryCodeByTimezone(city); + logInfo(`The country code for ${city} is ${countryCode}`); + + // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin + try { + location = new URL(bidderRequest.refererInfo.page) + winTop = window.top; + } catch (e) { + location = winTop.location; + logMessage(e); + }; + + let request = { + id: validBidRequests[0].bidderRequestId, + + imp: validBidRequests.map(request => { + const { bidId, sizes, mediaType, ortb2 } = request + const item = { + id: bidId, + region: request.params.region, + traffic: mediaType, + bidFloor: getBidFloor(request), + ortb2: ortb2 + + } + + if (request.mediaTypes.banner) { + item.banner = { + format: (request.mediaTypes.banner.sizes || sizes).map(size => { + return { w: size[0], h: size[1] } + }), + } + } + + if (request.schain) { + item.schain = request.schain; + } + + if (request.floorData) { + item.bidFloor = request.floorData.floorMin; + } + return item + }), + auctionId: validBidRequests[0].auctionId, + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language : '', + geo: navigator.geolocation.getCurrentPosition(position => { + const { latitude, longitude } = position.coords; + return { + latitude: latitude, + longitude: longitude + } + // Show a map centered at latitude / longitude. + }) || { utcoffset: new Date().getTimezoneOffset() }, + city: city, + 'host': location.host, + 'page': location.pathname, + 'coppa': config.getConfig('coppa') === true ? 1 : 0 + // userId: validBidRequests[0].userId + }; + + request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent + } + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent; + } + } + + return { + method: 'POST', + url: AD_URL, + data: request, + + }; + }, + + interpretResponse: function (serverResponse) { + const response = serverResponse.body + + const bids = [] + + response.seatbid.forEach(seat => { + seat.bid.forEach(bid => { + bids.push({ + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.crid, + ad: bid.adm, + currency: 'USD', + netRevenue: true, + ttl: 300, + meta: { + advertiserDomains: bid.adomain || [], + }, + }) + }) + }) + + return bids + }, + + getUserSyncs: (syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = '') => { + let syncs = []; + let { gdprApplies, consentString = '' } = gdprConsent; + + if (serverResponses.length > 0) { + logInfo('preciso bidadapter getusersync serverResponses:' + serverResponses.toString); + } + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `${URL_SYNC}id=${userId}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&us_privacy=${uspConsent}&t=4` + }); + } else { + syncs.push({ + type: 'image', + url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&us_privacy=${uspConsent}&t=2` + }); + } + + return syncs + } + +}; + +function getCountryCodeByTimezone(city) { + try { + const now = new Date(); + const options = { + timeZone: city, + timeZoneName: 'long', + }; + const [timeZoneName] = new Intl.DateTimeFormat('en-US', options) + .formatToParts(now) + .filter((part) => part.type === 'timeZoneName'); + + if (timeZoneName) { + // Extract the country code from the timezone name + const parts = timeZoneName.value.split('-'); + if (parts.length >= 2) { + return parts[1]; + } + } + } catch (error) { + // Handle errors, such as an invalid timezone city + logInfo(error); + } + + // Handle the case where the city is not found or an error occurred + return 'Unknown'; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidFloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0 + } +} + +registerBidder(spec); diff --git a/modules/precisoBidAdapter.md b/modules/precisoBidAdapter.md new file mode 100644 index 00000000000..b1fb0d062da --- /dev/null +++ b/modules/precisoBidAdapter.md @@ -0,0 +1,84 @@ +# Overview + +``` +Module Name: Preciso Bidder Adapter +Module Type: Bidder Adapter +Maintainer: tech@preciso.net +``` + +# Description + +Module that connects to preciso' demand sources + +# Parameters + +| Name | Scope | Description | Example | +| :------------ | :------- | :------------------------ | :------------------- | +| `region` | required (for prebid.js) | region | "prebid-eu" | +| `publisherId` | required (for prebid-server) | partner ID | "1901" | +| `traffic` | optional (for prebid.js) | Configures the mediaType that should be used. Values can be banner, native or video | "banner" | + +# Test Parameters +``` + var adUnits = [ + // Will return static native ad. Assets are stored through user UI for each placement separetly + { + code: 'placementId_0', + mediaTypes: { + native: {} + }, + bids: [ + { + bidder: 'preciso', + params: { + host: 'prebid', + publisherId: '0', + region: 'prebid-eu', + traffic: 'native' + } + } + ] + }, + // Will return static test banner + { + code: 'placementId_0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'preciso', + params: { + host: 'prebid', + publisherId: '0', + region: 'prebid-eu', + traffic: 'banner' + } + } + ] + }, + // Will return test vast xml. All video params are stored under placement in publishers UI + { + code: 'placementId_0', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'preciso', + params: { + host: 'prebid', + publisherId: '0', + region: 'prebid-eu', + traffic: 'video' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 37167fff691..4ec8a8e4b7e 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -4,7 +4,6 @@ import { deepClone, deepSetValue, generateUUID, - getGptSlotInfoForAdUnitCode, getParameterByName, isNumber, logError, @@ -13,7 +12,8 @@ import { mergeDeep, parseGPTSingleSizeArray, parseUrl, - pick + pick, + deepEqual } from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {config} from '../src/config.js'; @@ -27,8 +27,9 @@ import {bidderSettings} from '../src/bidderSettings.js'; import {auctionManager} from '../src/auctionManager.js'; import {IMP, PBS, registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.js'; -import {beConvertCurrency} from '../src/utils/currency.js'; import {adjustCpm} from '../src/utils/cpm.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import {convertCurrency} from '../libraries/currencyUtils/currency.js'; /** * @summary This Module is intended to provide users with the ability to dynamically set and enforce price floors on a per auction basis. @@ -40,10 +41,13 @@ const MODULE_NAME = 'Price Floors'; */ const ajax = ajaxBuilder(10000); +// eslint-disable-next-line symbol-description +const SYN_FIELD = Symbol(); + /** * @summary Allowed fields for rules to have */ -export let allowedFields = ['gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType']; +export let allowedFields = [SYN_FIELD, 'gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType']; /** * @summary This is a flag to indicate if a AJAX call is processing for a floors request @@ -104,6 +108,7 @@ function getAdUnitCode(request, response, {index = auctionManager.index} = {}) { * @summary floor field types with their matching functions to resolve the actual matched value */ export let fieldMatchingFunctions = { + [SYN_FIELD]: () => '*', 'size': (bidRequest, bidResponse) => parseGPTSingleSizeArray(bidResponse.size) || '*', 'mediaType': (bidRequest, bidResponse) => bidResponse.mediaType || 'banner', 'gptSlot': (bidRequest, bidResponse) => getGptSlotFromAdUnit((bidRequest || bidResponse).transactionId) || getGptSlotInfoForAdUnitCode(getAdUnitCode(bidRequest, bidResponse)).gptSlot, @@ -117,6 +122,7 @@ export let fieldMatchingFunctions = { * Returns array of Tuple [exact match, catch all] for each field in rules file */ function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) { + if (!floorFields.length) return []; // generate combination of all exact matches and catch all for each field type return floorFields.reduce((accum, field) => { let exactMatch = fieldMatchingFunctions[field](bidObject, responseObject) || '*'; @@ -132,7 +138,9 @@ function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) { */ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) { let fieldValues = enumeratePossibleFieldValues(deepAccess(floorData, 'schema.fields') || [], bidObject, responseObject); - if (!fieldValues.length) return { matchingFloor: floorData.default }; + if (!fieldValues.length) { + return {matchingFloor: undefined} + } // look to see if a request for this context was made already let matchingInput = fieldValues.map(field => field[0]).join('-'); @@ -146,9 +154,9 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let matchingData = { floorMin: floorData.floorMin || 0, - floorRuleValue: isNaN(floorData.values[matchingRule]) ? floorData.default : floorData.values[matchingRule], + floorRuleValue: floorData.values[matchingRule], matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters - matchingRule + matchingRule: matchingRule === floorData.meta?.defaultRule ? undefined : matchingRule }; // use adUnit floorMin as priority! const floorMin = deepAccess(bidObject, 'ortb2Imp.ext.prebid.floors.floorMin'); @@ -300,14 +308,20 @@ function normalizeRulesForAuction(floorData, adUnitCode) { * Only called if no set config or fetch level data has returned */ export function getFloorDataFromAdUnits(adUnits) { + const schemaAu = adUnits.find(au => au.floors?.schema != null); return adUnits.reduce((accum, adUnit) => { - if (isFloorsDataValid(adUnit.floors)) { + if (adUnit.floors?.schema != null && !deepEqual(adUnit.floors.schema, schemaAu?.floors?.schema)) { + logError(`${MODULE_NAME}: adUnit '${adUnit.code}' declares a different schema from one previously declared by adUnit '${schemaAu.code}'. Floor config for '${adUnit.code}' will be ignored.`) + return accum; + } + const floors = Object.assign({}, schemaAu?.floors, {values: undefined}, adUnit.floors) + if (isFloorsDataValid(floors)) { // if values already exist we want to not overwrite them if (!accum.values) { - accum = getFloorsDataForAuction(adUnit.floors, adUnit.code); + accum = getFloorsDataForAuction(floors, adUnit.code); accum.location = 'adUnit'; } else { - let newRules = getFloorsDataForAuction(adUnit.floors, adUnit.code).values; + let newRules = getFloorsDataForAuction(floors, adUnit.code).values; // copy over the new rules into our values object Object.assign(accum.values, newRules); } @@ -417,7 +431,7 @@ function validateSchemaFields(fields) { if (Array.isArray(fields) && fields.length > 0 && fields.every(field => allowedFields.indexOf(field) !== -1)) { return true; } - logError(`${MODULE_NAME}: Fields recieved do not match allowed fields`); + logError(`${MODULE_NAME}: Fields received do not match allowed fields`); return false; } @@ -443,7 +457,26 @@ function validateRules(floorsData, numFields, delimiter) { return Object.keys(floorsData.values).length > 0; } +export function normalizeDefault(model) { + if (isNumber(model.default)) { + let defaultRule = '*'; + const numFields = (model.schema?.fields || []).length; + if (!numFields) { + deepSetValue(model, 'schema.fields', [SYN_FIELD]); + } else { + defaultRule = Array(numFields).fill('*').join(model.schema?.delimiter || '|'); + } + model.values = model.values || {}; + if (model.values[defaultRule] == null) { + model.values[defaultRule] = model.default; + model.meta = {defaultRule}; + } + } + return model; +} + function modelIsValid(model) { + model = normalizeDefault(model); // schema.fields has only allowed attributes if (!validateSchemaFields(deepAccess(model, 'schema.fields'))) { return false; @@ -583,7 +616,7 @@ function handleFetchError(status) { } /** - * This function handles sending and recieving the AJAX call for a floors fetch + * This function handles sending and receiving the AJAX call for a floors fetch * @param {object} floorsConfig the floors config coming from setConfig */ export function generateAndHandleFetch(floorEndpoint) { @@ -715,7 +748,7 @@ export const addBidResponseHook = timedBidResponseHook('priceFloors', function a let floorInfo = getFirstMatchingFloor(floorData.data, matchingBidRequest, {...bid, size: [bid.width, bid.height]}); if (!floorInfo.matchingFloor) { - logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); + if (floorInfo.matchingFloor !== 0) logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); return fn.call(this, adUnitCode, bid, reject); } @@ -794,8 +827,8 @@ export function setImpExtPrebidFloors(imp, bidRequest, context) { if (floorMinCur == null) { floorMinCur = imp.bidfloorcur } const ortb2ImpFloorCur = imp.ext?.prebid?.floors?.floorMinCur || imp.ext?.prebid?.floorMinCur || floorMinCur; const ortb2ImpFloorMin = imp.ext?.prebid?.floors?.floorMin || imp.ext?.prebid?.floorMin; - const convertedFloorMinValue = beConvertCurrency(imp.bidfloor, imp.bidfloorcur, floorMinCur); - const convertedOrtb2ImpFloorMinValue = ortb2ImpFloorMin && ortb2ImpFloorCur ? beConvertCurrency(ortb2ImpFloorMin, ortb2ImpFloorCur, floorMinCur) : false; + const convertedFloorMinValue = convertCurrency(imp.bidfloor, imp.bidfloorcur, floorMinCur); + const convertedOrtb2ImpFloorMinValue = ortb2ImpFloorMin && ortb2ImpFloorCur ? convertCurrency(ortb2ImpFloorMin, ortb2ImpFloorCur, floorMinCur) : false; const lowestImpFloorMin = convertedOrtb2ImpFloorMinValue && convertedOrtb2ImpFloorMinValue < convertedFloorMinValue ? convertedOrtb2ImpFloorMinValue diff --git a/modules/prismaBidAdapter.js b/modules/prismaBidAdapter.js index 7c9108f60b1..c13e6e1c330 100644 --- a/modules/prismaBidAdapter.js +++ b/modules/prismaBidAdapter.js @@ -2,7 +2,7 @@ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {getANKeywordParam} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; const BIDDER_CODE = 'prisma'; const BIDDER_URL = 'https://prisma.nexx360.io/prebid'; diff --git a/modules/programmaticaBidAdapter.js b/modules/programmaticaBidAdapter.js new file mode 100644 index 00000000000..7d52e305189 --- /dev/null +++ b/modules/programmaticaBidAdapter.js @@ -0,0 +1,153 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +import { deepAccess, parseSizesInput, isArray } from '../src/utils.js'; + +const BIDDER_CODE = 'programmatica'; +const DEFAULT_ENDPOINT = 'asr.programmatica.com'; +const SYNC_ENDPOINT = 'sync.programmatica.com'; +const ADOMAIN = 'programmatica.com'; +const TIME_TO_LIVE = 360; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + let valid = bid.params.siteId && bid.params.placementId; + + return !!valid; + }, + + buildRequests: function(validBidRequests, bidderRequest) { + let requests = []; + for (const bid of validBidRequests) { + let endpoint = bid.params.endpoint || DEFAULT_ENDPOINT; + + requests.push({ + method: 'GET', + url: `https://${endpoint}/get`, + data: { + site_id: bid.params.siteId, + placement_id: bid.params.placementId, + prebid: true, + }, + bidRequest: bid, + }); + } + + return requests; + }, + + interpretResponse: function(serverResponse, request) { + if (!serverResponse?.body?.content?.data) { + return []; + } + + const bidResponses = []; + const body = serverResponse.body; + + let mediaType = BANNER; + let ad, vastXml; + let width; + let height; + + let sizes = getSize(body.size); + if (isArray(sizes)) { + [width, height] = sizes; + } + + if (body.type.format != '') { + // banner + ad = body.content.data; + if (body.content.imps?.length) { + for (const imp of body.content.imps) { + ad += ``; + } + } + } else { + // video + vastXml = body.content.data; + mediaType = VIDEO; + + if (!width || !height) { + const pSize = deepAccess(request.bidRequest, 'mediaTypes.video.playerSize'); + const reqSize = getSize(pSize); + if (isArray(reqSize)) { + [width, height] = reqSize; + } + } + } + + const bidResponse = { + requestId: request.bidRequest.bidId, + cpm: body.cpm, + currency: body.currency || 'USD', + width: parseInt(width), + height: parseInt(height), + creativeId: body.id, + netRevenue: true, + ttl: TIME_TO_LIVE, + ad: ad, + mediaType: mediaType, + vastXml: vastXml, + meta: { + advertiserDomains: [ADOMAIN], + } + }; + + if ((mediaType === VIDEO && request.bidRequest.mediaTypes?.video) || (mediaType === BANNER && request.bidRequest.mediaTypes?.banner)) { + bidResponses.push(bidResponse); + } + + return bidResponses; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + if (!hasPurpose1Consent(gdprConsent)) { + return syncs; + } + + let params = `usp=${uspConsent ?? ''}&consent=${gdprConsent?.consentString ?? ''}`; + if (typeof gdprConsent?.gdprApplies === 'boolean') { + params += `&gdpr=${Number(gdprConsent.gdprApplies)}`; + } + + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `//${SYNC_ENDPOINT}/match/sp.ifr?${params}` + }); + } + + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: `//${SYNC_ENDPOINT}/match/sp?${params}` + }); + } + + return syncs; + }, + + onTimeout: function(timeoutData) {}, + onBidWon: function(bid) {}, + onSetTargeting: function(bid) {}, + onBidderError: function() {}, + supportedMediaTypes: [ BANNER, VIDEO ] +} + +registerBidder(spec); + +function getSize(paramSizes) { + const parsedSizes = parseSizesInput(paramSizes); + const sizes = parsedSizes.map(size => { + const [width, height] = size.split('x'); + const w = parseInt(width, 10); + const h = parseInt(height, 10); + return [w, h]; + }); + + return sizes[0] || null; +} diff --git a/modules/programmaticaBidAdapter.md b/modules/programmaticaBidAdapter.md new file mode 100644 index 00000000000..5982edf143e --- /dev/null +++ b/modules/programmaticaBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: Programmatica Bid Adapter +Module Type: Bidder Adapter +Maintainer: tech@programmatica.com +``` + +# Description +Connects to Programmatica server for bids. +Module supports banner and video mediaType. + +# Test Parameters + +``` + var adUnits = [{ + code: '/test/div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'programmatica', + params: { + siteId: 'cga9l34ipgja79esubrg', + placementId: 'cgim20sipgj0vj1cb510' + } + }] + }, + { + code: '/test/div', + mediaTypes: { + video: { + playerSize: [[640, 360]] + } + }, + bids: [{ + bidder: 'programmatica', + params: { + siteId: 'cga9l34ipgja79esubrg', + placementId: 'cioghpcipgj8r721e9ag' + } + }] + },]; +``` diff --git a/modules/pubCircleBidAdapter.js b/modules/pubCircleBidAdapter.js new file mode 100644 index 00000000000..54224fd0403 --- /dev/null +++ b/modules/pubCircleBidAdapter.js @@ -0,0 +1,231 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'pubcircle'; +const AD_URL = 'https://ml.pubcircle.ai/pbjs'; +const SYNC_URL = 'https://cs.pubcircle.ai'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + placement.placementId = placementId; + placement.type = 'publisher'; + + if (bid.userId) { + getUserId(placement.eids, bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com'); + getUserId(placement.eids, bid.userId.lotamePanoramaId, 'lotame.com'); + getUserId(placement.eids, bid.userId.idx, 'idx.lat'); + getUserId(placement.eids, bid.userId.idl_env, 'liveramp.com'); + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +function getUserId(eids, id, source, uidExt) { + if (id) { + var uid = { id }; + if (uidExt) { + uid.ext = uidExt; + } + eids.push({ + source, + uids: [ uid ] + }); + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && params.placementId); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + }, + + onBidViewable: function (bid) { + // to do : we need to implement js tag to fire pixel with viewability counter + } +}; + +registerBidder(spec); diff --git a/modules/pubCircleBidAdapter.md b/modules/pubCircleBidAdapter.md new file mode 100644 index 00000000000..4fc114bf20c --- /dev/null +++ b/modules/pubCircleBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: PubCirlce Bidder Adapter +Module Type: PubCirlce Bidder Adapter +Maintainer: system@smartyads.com +``` + +# Description + +Connects to PubCirlce exchange for bids. +PubCirlce bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'addunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'pubcircle', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'pubcircle', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'pubcircle', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/publinkIdSystem.js b/modules/publinkIdSystem.js index 6a5504b5ba0..1a993c99b45 100644 --- a/modules/publinkIdSystem.js +++ b/modules/publinkIdSystem.js @@ -16,6 +16,8 @@ const MODULE_NAME = 'publinkId'; const GVLID = 24; const PUBLINK_COOKIE = '_publink'; const PUBLINK_S2S_COOKIE = '_publink_srv'; +const PUBLINK_REQUEST_PATH = '/cvx/client/sync/publink'; +const PUBLINK_REFRESH_PATH = '/cvx/client/sync/publink/refresh'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); @@ -23,10 +25,9 @@ function isHex(s) { return /^[A-F0-9]+$/i.test(s); } -function publinkIdUrl(params, consentData) { - let url = parseUrl('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink'); +function publinkIdUrl(params, consentData, storedId) { + let url = parseUrl('https://proc.ad.cpe.dotomi.com' + PUBLINK_REFRESH_PATH); url.search = { - deh: params.e, mpn: 'Prebid.js', mpv: '$prebid.version$', }; @@ -36,9 +37,21 @@ function publinkIdUrl(params, consentData) { url.search.gdpr_consent = consentData.consentString; } - if (params.site_id) { url.search.sid = params.site_id; } + if (params) { + if (params.e) { + // if there's an email parameter call the request path + url.search.deh = params.e; + url.pathname = PUBLINK_REQUEST_PATH; + } + + if (params.site_id) { url.search.sid = params.site_id; } - if (params.api_key) { url.search.apikey = params.api_key; } + if (params.api_key) { url.search.apikey = params.api_key; } + } + + if (storedId) { + url.search.publink = storedId; + } const usPrivacyString = uspDataHandler.getConsentData(); if (usPrivacyString && typeof usPrivacyString === 'string') { @@ -48,7 +61,7 @@ function publinkIdUrl(params, consentData) { return buildUrl(url); } -function makeCallback(config = {}, consentData) { +function makeCallback(config = {}, consentData, storedId) { return function(prebidCallback) { const options = {method: 'GET', withCredentials: true}; let handleResponse = function(responseText, xhr) { @@ -59,15 +72,12 @@ function makeCallback(config = {}, consentData) { } } }; - - if (config.params && config.params.e) { - if (isHex(config.params.e)) { - ajax(publinkIdUrl(config.params, consentData), handleResponse, undefined, options); - } else { - logError('params.e must be a hex string'); - } + if ((config.params && config.params.e && isHex(config.params.e)) || storedId) { + ajax(publinkIdUrl(config.params, consentData, storedId), handleResponse, undefined, options); + } else if (config.params.e) { + logError('params.e must be a hex string'); } - }; + } } function getlocalValue() { @@ -137,9 +147,13 @@ export const publinkIdSubmodule = { if (localValue) { return {id: localValue}; } - if (!storedId) { - return {callback: makeCallback(config, consentData)}; - } - } + return {callback: makeCallback(config, consentData, storedId)}; + }, + eids: { + 'publinkId': { + source: 'epsilon.com', + atype: 3 + }, + }, }; submodule('userId', publinkIdSubmodule); diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index acae93c57be..a8f2b8d20ee 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -1,10 +1,11 @@ -import { _each, pick, logWarn, isStr, isArray, logError, getGptSlotInfoForAdUnitCode } from '../src/utils.js'; +import {_each, isArray, isStr, logError, logWarn, pick} from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { ajax } from '../src/ajax.js'; -import { config } from '../src/config.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; /// /////////// CONSTANTS ////////////// const ADAPTER_CODE = 'pubmatic'; @@ -22,6 +23,7 @@ const ERROR = 'error'; const REQUEST_ERROR = 'request-error'; const TIMEOUT_ERROR = 'timeout-error'; const EMPTY_STRING = ''; +const OPEN_AUCTION_DEAL_ID = '-1'; const MEDIA_TYPE_BANNER = 'banner'; const CURRENCY_USD = 'USD'; const BID_PRECISION = 2; @@ -91,7 +93,7 @@ function copyRequiredBidDetails(bid) { 'bidderCode', 'adapterCode', 'bidId', - 'status', () => NO_BID, // default a bid to NO_BID until response is recieved or bid is timed out + 'status', () => NO_BID, // default a bid to NO_BID until response is received or bid is timed out 'finalSource as source', 'params', 'floorData', @@ -270,9 +272,10 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'psz': bid.bidResponse ? (bid.bidResponse.dimensions.width + 'x' + bid.bidResponse.dimensions.height) : '0x0', 'eg': bid.bidResponse ? bid.bidResponse.bidGrossCpmUSD : 0, 'en': bid.bidResponse ? bid.bidResponse.bidPriceUSD : 0, - 'di': bid.bidResponse ? (bid.bidResponse.dealId || EMPTY_STRING) : EMPTY_STRING, + 'di': bid.bidResponse ? (bid.bidResponse.dealId || OPEN_AUCTION_DEAL_ID) : OPEN_AUCTION_DEAL_ID, 'dc': bid.bidResponse ? (bid.bidResponse.dealChannel || EMPTY_STRING) : EMPTY_STRING, - 'l1': bid.bidResponse ? bid.clientLatencyTimeMs : 0, + 'l1': bid.bidResponse ? bid.partnerTimeToRespond : 0, + 'ol1': bid.bidResponse ? bid.clientLatencyTimeMs : 0, 'l2': 0, 'adv': bid.bidResponse ? getAdDomain(bid.bidResponse) || undefined : undefined, 'ss': isS2SBidder(bid.bidder), @@ -283,7 +286,7 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'ocpm': bid.bidResponse ? (bid.bidResponse.originalCpm || 0) : 0, 'ocry': bid.bidResponse ? (bid.bidResponse.originalCurrency || CURRENCY_USD) : CURRENCY_USD, 'piid': bid.bidResponse ? (bid.bidResponse.partnerImpId || EMPTY_STRING) : EMPTY_STRING, - 'frv': (s2sBidders.indexOf(bid.bidder) > -1) ? undefined : (bid.bidResponse ? (bid.bidResponse.floorData ? bid.bidResponse.floorData.floorRuleValue : undefined) : undefined), + 'frv': (bid.bidResponse ? (bid.bidResponse.floorData ? bid.bidResponse.floorData.floorRuleValue : undefined) : undefined), 'md': bid.bidResponse ? getMetadata(bid.bidResponse.meta) : undefined }); }); @@ -319,6 +322,17 @@ function getTgId() { return 0; } +function getFloorFetchStatus(floorData) { + if (!floorData?.floorRequestData) { + return false; + } + const { location, fetchStatus } = floorData?.floorRequestData; + const isDataValid = location !== CONSTANTS.FLOOR_VALUES.NO_DATA; + const isFetchSuccessful = location === CONSTANTS.FLOOR_VALUES.FETCH && fetchStatus === CONSTANTS.FLOOR_VALUES.SUCCESS; + const isAdUnitOrSetConfig = location === CONSTANTS.FLOOR_VALUES.AD_UNIT || location === CONSTANTS.FLOOR_VALUES.SET_CONFIG; + return isDataValid && (isAdUnitOrSetConfig || isFetchSuccessful); +} + function executeBidsLoggerCall(e, highestCpmBids) { let auctionId = e.auctionId; let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; @@ -326,6 +340,8 @@ function executeBidsLoggerCall(e, highestCpmBids) { let floorData = auctionCache.floorData; let outputObj = { s: [] }; let pixelURL = END_POINT_BID_LOGGER; + // will return true if floor data is present. + let fetchStatus = getFloorFetchStatus(auctionCache.floorData); if (!auctionCache) { return; @@ -346,8 +362,9 @@ function executeBidsLoggerCall(e, highestCpmBids) { outputObj['pdvid'] = '' + profileVersionId; outputObj['dvc'] = {'plt': getDevicePlatform()}; outputObj['tgid'] = getTgId(); + outputObj['pbv'] = getGlobal()?.version || '-1'; - if (floorData) { + if (floorData && fetchStatus) { outputObj['fmv'] = floorData.floorRequestData ? floorData.floorRequestData.modelVersion || undefined : undefined; outputObj['ft'] = floorData.floorResponseData ? (floorData.floorResponseData.enforcements.enforceJS == false ? 0 : 1) : undefined; } @@ -362,7 +379,7 @@ function executeBidsLoggerCall(e, highestCpmBids) { 'mt': getAdUnitAdFormats(origAdUnit), 'sz': getSizesForAdUnit(adUnit, adUnitId), 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestCpmBids.filter(bid => bid.adUnitCode === adUnitId)), - 'fskp': floorData ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, + 'fskp': (floorData && fetchStatus) ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, }; slotsArray.push(slotObject); return slotsArray; @@ -415,6 +432,7 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { pixelURL += '&eg=' + enc(winningBid.bidResponse.bidGrossCpmUSD); pixelURL += '&kgpv=' + enc(getValueForKgpv(winningBid, adUnitId)); pixelURL += '&piid=' + enc(winningBid.bidResponse.partnerImpId || EMPTY_STRING); + pixelURL += '&di=' + enc(winningBid?.bidResponse?.dealId || OPEN_AUCTION_DEAL_ID); pixelURL += '&plt=' + enc(getDevicePlatform()); pixelURL += '&psz=' + enc((winningBid?.bidResponse?.dimensions?.width || '0') + 'x' + @@ -492,10 +510,24 @@ function bidResponseHandler(args) { bid.adId = args.adId; bid.source = formatSource(bid.source || args.source); setBidStatus(bid, args); + const latency = args?.timeToRespond || Date.now() - cache.auctions[args.auctionId].timestamp; + const auctionTime = cache.auctions[args.auctionId].timeout; + // Check if latency is greater than auctiontime+150, then log auctiontime+150 to avoid large numbers + bid.partnerTimeToRespond = latency > (auctionTime + 150) ? (auctionTime + 150) : latency; bid.clientLatencyTimeMs = Date.now() - cache.auctions[args.auctionId].timestamp; bid.bidResponse = parseBidResponse(args); } +function bidRejectedHandler(args) { + // If bid is rejected due to floors value did not met + // make cpm as 0, status as bidRejected and forward the bid for logging + if (args.rejectionReason === CONSTANTS.REJECTION_REASON.FLOOR_NOT_MET) { + args.cpm = 0; + args.status = CONSTANTS.BID_STATUS.BID_REJECTED; + bidResponseHandler(args); + } +} + function bidderDoneHandler(args) { cache.auctions[args.auctionId].bidderDonePendingCount--; args.bids.forEach(bid => { @@ -594,6 +626,9 @@ let pubmaticAdapter = Object.assign({}, baseAdapter, { case CONSTANTS.EVENTS.BID_RESPONSE: bidResponseHandler(args); break; + case CONSTANTS.EVENTS.BID_REJECTED: + bidRejectedHandler(args) + break; case CONSTANTS.EVENTS.BIDDER_DONE: bidderDoneHandler(args); break; diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index eb2ad8ccb03..ee80de03c03 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -1,10 +1,11 @@ -import { getBidRequest, logWarn, isBoolean, isStr, isArray, inIframe, mergeDeep, deepAccess, isNumber, deepSetValue, logInfo, logError, deepClone, convertTypes, uniques, isPlainObject, isInteger } from '../src/utils.js'; +import { getBidRequest, logWarn, isBoolean, isStr, isArray, inIframe, mergeDeep, deepAccess, isNumber, deepSetValue, logInfo, logError, deepClone, uniques, isPlainObject, isInteger } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE, ADPOD } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { bidderSettings } from '../src/bidderSettings.js'; import CONSTANTS from '../src/constants.json'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'pubmatic'; const LOG_WARN_PREFIX = 'PubMatic: '; @@ -757,6 +758,9 @@ function _addImpressionFPD(imp, bid) { deepSetValue(imp, `ext.data.${prop}`, ortb2[prop]); } }); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); + gpid && deepSetValue(imp, `ext.gpid`, gpid); } function _addFloorFromFloorModule(impObj, bid) { @@ -1150,10 +1154,7 @@ export const spec = { payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED); payload.user.geo = {}; // TODO: fix lat and long to only come from request object, not params - payload.user.geo.lat = _parseSlotParam('lat', 0); - payload.user.geo.lon = _parseSlotParam('lon', 0); payload.user.yob = _parseSlotParam('yob', conf.yob); - payload.device.geo = payload.user.geo; payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim(); payload.site.domain = _getDomainFromURL(payload.site.page); @@ -1194,6 +1195,15 @@ export const spec = { deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); } + // Attaching GPP Consent Params + if (bidderRequest?.gppConsent?.gppString) { + deepSetValue(payload, 'regs.gpp', bidderRequest.gppConsent.gppString); + deepSetValue(payload, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections); + } else if (bidderRequest?.ortb2?.regs?.gpp) { + deepSetValue(payload, 'regs.gpp', bidderRequest.ortb2.regs.gpp); + deepSetValue(payload, 'regs.gpp_sid', bidderRequest.ortb2.regs.gpp_sid); + } + // coppa compliance if (config.getConfig('coppa') === true) { deepSetValue(payload, 'regs.coppa', 1); @@ -1203,22 +1213,36 @@ export const spec = { // First Party Data const commonFpd = (bidderRequest && bidderRequest.ortb2) || {}; - if (commonFpd.site) { + const { user, device, site, bcat } = commonFpd; + if (site) { const { page, domain, ref } = payload.site; - mergeDeep(payload, {site: commonFpd.site}); + mergeDeep(payload, {site: site}); payload.site.page = page; payload.site.domain = domain; payload.site.ref = ref; } - if (commonFpd.user) { - mergeDeep(payload, {user: commonFpd.user}); + if (user) { + mergeDeep(payload, {user: user}); } - if (commonFpd.bcat) { - blockedIabCategories = blockedIabCategories.concat(commonFpd.bcat); + if (bcat) { + blockedIabCategories = blockedIabCategories.concat(bcat); } // check if fpd ortb2 contains device property with sua object - if (commonFpd.device?.sua) { - payload.device.sua = commonFpd.device?.sua; + if (device?.sua) { + payload.device.sua = device?.sua; + } + + if (device?.ext?.cdep) { + deepSetValue(payload, 'device.ext.cdep', device.ext.cdep); + } + + if (user?.geo && device?.geo) { + payload.device.geo = { ...payload.device.geo, ...device.geo }; + payload.user.geo = { ...payload.user.geo, ...user.geo }; + } else { + if (user?.geo || device?.geo) { + payload.user.geo = payload.device.geo = (user?.geo ? { ...payload.user.geo, ...user.geo } : { ...payload.user.geo, ...device.geo }); + } } if (commonFpd.ext?.prebid?.bidderparams?.[bidderRequest.bidderCode]?.acat) { @@ -1343,6 +1367,21 @@ export const spec = { }); }); } + let fledgeAuctionConfigs = deepAccess(response.body, 'ext.fledge_auction_configs'); + if (fledgeAuctionConfigs) { + fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { + return { + bidId, + config: Object.assign({ + auctionSignals: {}, + }, cfg) + } + }); + return { + bids: bidResponses, + fledgeAuctionConfigs, + } + } } catch (error) { logError(error); } @@ -1352,7 +1391,7 @@ export const spec = { /** * Register User Sync. */ - getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent, gppConsent) => { let syncurl = '' + publisherId; // Attaching GDPR Consent Params in UserSync url @@ -1366,6 +1405,12 @@ export const spec = { syncurl += '&us_privacy=' + encodeURIComponent(uspConsent); } + // GPP Consent + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + syncurl += '&gpp=' + encodeURIComponent(gppConsent.gppString); + syncurl += '&gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(',')); + } + // coppa compliance if (config.getConfig('coppa') === true) { syncurl += '&coppa=1'; diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index 19a3c236942..e97e5505768 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -1,9 +1,10 @@ -import { deepAccess, getGptSlotInfoForAdUnitCode, parseSizesInput, getWindowLocation, buildUrl } from '../src/utils.js'; +import { deepAccess, parseSizesInput, getWindowLocation, buildUrl } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index 015e80d5692..516254b358b 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -1,21 +1,10 @@ -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -/* eslint dot-notation:0, quote-props:0 */ -import {convertTypes, deepAccess, isArray, isFn, logError} from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import {isArray} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {Renderer} from '../src/Renderer.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; -const NATIVE_DEFAULTS = { - TITLE_LEN: 100, - DESCR_LEN: 200, - SPONSORED_BY_LEN: 50, - IMG_MIN: 150, - ICON_MIN: 50, -}; - -const DEFAULT_BID_TTL = 20; const DEFAULT_CURRENCY = 'USD'; -const DEFAULT_NET_REVENUE = true; -const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'video', 'battr', 'bcat', 'badv', 'bidfloor']; +const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'battr', 'deals']; const DEFAULT_TMAX = 500; /** @@ -41,33 +30,21 @@ export const spec = { ), buildRequests: (bidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - bidRequests = convertOrtbRequestToProprietaryNative(bidRequests); - - const request = { - id: bidRequests[0].bidderRequestId, - imp: bidRequests.map(slot => impression(slot)), - site: site(bidRequests, bidderRequest), - app: app(bidRequests), - device: device(), - bcat: deepAccess(bidderRequest.ortb2Imp, 'bcat') || bidRequests[0].params.bcat, - badv: bidRequests[0].params.badv, - user: user(bidRequests[0], bidderRequest), - regs: regs(bidderRequest), - source: source(bidRequests[0].schain), - tmax: bidderRequest.timeout || DEFAULT_TMAX, - }; + const data = converter.toORTB({bidRequests, bidderRequest}); return { method: 'POST', url: 'https://bid.contextweb.com/header/ortb?src=prebid', - data: request, + data, bidderRequest }; }, - interpretResponse: (response, request) => ( - bidResponseAvailable(request, response) - ), + interpretResponse: (response, request) => { + if (response.body) { + return converter.fromORTB({response: response.body, request: request.data}).bids; + } + return []; + }, getUserSyncs: syncOptions => { if (syncOptions.iframeEnabled) { @@ -82,7 +59,7 @@ export const spec = { }]; } }, - transformBidParams: function(params, isOpenRtb) { + transformBidParams: function(params) { return convertTypes({ 'cf': 'string', 'cp': 'number', @@ -91,124 +68,61 @@ export const spec = { } }; -/** - * Callback for bids, after the call to PulsePoint completes. - */ -function bidResponseAvailable(request, response) { - const idToImpMap = {}; - const idToBidMap = {}; - const idToSlotConfig = {}; - const bidResponse = response.body; - // extract the request bids and the response bids, keyed by impr-id - const ortbRequest = request.data; - ortbRequest.imp.forEach(imp => { - idToImpMap[imp.id] = imp; - }); - if (bidResponse) { - bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => { - idToBidMap[bid.impid] = bid; - })); - } - if (request.bidderRequest && request.bidderRequest.bids) { - request.bidderRequest.bids.forEach(bid => { - idToSlotConfig[bid.bidId] = bid; - }); - } - const bids = []; - Object.keys(idToImpMap).forEach(id => { - if (idToBidMap[id]) { - const bid = { - requestId: id, - cpm: idToBidMap[id].price, - creative_id: idToBidMap[id].crid, - creativeId: idToBidMap[id].crid, - adId: id, - ttl: idToBidMap[id].exp || DEFAULT_BID_TTL, - netRevenue: DEFAULT_NET_REVENUE, - currency: bidResponse.cur || DEFAULT_CURRENCY, - meta: { advertiserDomains: idToBidMap[id].adomain || [] } - }; - if (idToImpMap[id].video) { - // for outstream, a renderer is specified - if (idToSlotConfig[id] && deepAccess(idToSlotConfig[id], 'mediaTypes.video.context') === 'outstream') { - bid.renderer = outstreamRenderer(deepAccess(idToSlotConfig[id], 'renderer.options'), deepAccess(idToBidMap[id], 'ext.outstream')); +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + currency: 'USD' + }, + + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + // tagid + imp.tagid = bidRequest.params.ct.toString(); + // unknown params + const unknownParams = slotUnknownParams(bidRequest); + if (imp.ext || unknownParams) { + imp.ext = Object.assign({}, imp.ext, unknownParams); + } + // battr + if (bidRequest.params.battr) { + ['banner', 'video', 'audio', 'native'].forEach(k => { + if (imp[k]) { + imp[k].battr = bidRequest.params.battr; } - bid.vastXml = idToBidMap[id].adm; - bid.mediaType = 'video'; - bid.width = idToBidMap[id].w; - bid.height = idToBidMap[id].h; - } else if (idToImpMap[id].banner) { - bid.ad = idToBidMap[id].adm; - bid.width = idToBidMap[id].w || idToImpMap[id].banner.w; - bid.height = idToBidMap[id].h || idToImpMap[id].banner.h; - } else if (idToImpMap[id]['native']) { - bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); - bid.mediaType = 'native'; - } - bids.push(bid); + }); } - }); - return bids; -} - -/** - * Produces an OpenRTBImpression from a slot config. - */ -function impression(slot) { - var firstPartyData = slot.ortb2Imp?.ext || {}; - var ext = Object.assign({}, firstPartyData, slotUnknownParams(slot)); - return { - id: slot.bidId, - banner: banner(slot), - 'native': nativeImpression(slot), - tagid: slot.params.ct.toString(), - video: video(slot), - bidfloor: bidFloor(slot), - ext: Object.keys(ext).length > 0 ? ext : null, - }; -} - -/** - * Produces an OpenRTB Banner object for the slot given. - */ -function banner(slot) { - const sizes = parseSizes(slot); - const size = adSize(slot, sizes); - return (slot.mediaTypes && slot.mediaTypes.banner) ? { - w: size[0], - h: size[1], - battr: slot.params.battr, - format: sizes - } : null; -} + // deals + if (bidRequest.params.deals && isArray(bidRequest.params.deals)) { + imp.pmp = { + private_auction: 0, + deals: bidRequest.params.deals + }; + } + return imp; + }, -/** - * Produce openrtb format objects based on the sizes configured for the slot. - */ -function parseSizes(slot) { - const sizes = deepAccess(slot, 'mediaTypes.banner.sizes'); - if (sizes && isArray(sizes)) { - return sizes.filter(sz => isArray(sz) && sz.length === 2).map(sz => ({ - w: sz[0], - h: sz[1] - })); - } - return null; -} + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + // publisher id + const siteOrApp = request.site || request.app; + const pubId = context.bidRequests && context.bidRequests.length > 0 ? context.bidRequests[0].params.cp : '0'; + if (siteOrApp) { + siteOrApp.publisher = Object.assign({}, siteOrApp.publisher, { + id: pubId.toString() + }); + } + // tmax + request.tmax = request.tmax || DEFAULT_TMAX; + return request; + }, -/** - * Produces an OpenRTB Video object for the slot given - */ -function video(slot) { - if (slot.params.video) { - return Object.assign({}, - slot.params.video, // previously supported as bidder param - slot.mediaTypes && slot.mediaTypes.video ? slot.mediaTypes.video : {}, // params on mediaTypes.video - {battr: slot.params.battr} - ); - } - return null; -} + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = bidResponse.cur || DEFAULT_CURRENCY; + return bidResponse; + }, +}); /** * Unknown params are captured and sent on ext @@ -225,278 +139,4 @@ function slotUnknownParams(slot) { return Object.keys(ext).length > 0 ? { prebid: ext } : null; } -/** - * Sets up the renderer on the bid, for outstream bid responses. - */ -function outstreamRenderer(rendererOptions, outstreamExtOptions) { - const renderer = Renderer.install({ - url: outstreamExtOptions.rendererUrl, - config: { - defaultOptions: outstreamExtOptions.config, - rendererOptions, - type: outstreamExtOptions.type - }, - loaded: false, - }); - renderer.setRender((bid) => { - bid.renderer.push(() => { - const config = bid.renderer.getConfig(); - new window.PulsePointOutstreamRenderer().render({ - adUnitCode: bid.adUnitCode, - vastXml: bid.vastXml, - type: config.type, - defaultOptions: config.defaultOptions, - rendererOptions - }); - }); - }); - return renderer; -} - -/** - * Produces an OpenRTB Native object for the slot given. - */ -function nativeImpression(slot) { - if (slot.nativeParams) { - const assets = []; - addAsset(assets, titleAsset(assets.length + 1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); - addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); - addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); - addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN)); - addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); - return { - request: JSON.stringify({ assets }), - ver: '1.1', - battr: slot.params.battr, - }; - } - return null; -} - -/** - * Helper method to add an asset to the assets list. - */ -function addAsset(assets, asset) { - if (asset) { - assets.push(asset); - } -} - -/** - * Produces a Native Title asset for the configuration given. - */ -function titleAsset(id, params, defaultLen) { - if (params) { - return { - id, - required: params.required ? 1 : 0, - title: { - len: params.len || defaultLen, - }, - }; - } - return null; -} - -/** - * Produces a Native Image asset for the configuration given. - */ -function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) { - return params ? { - id, - required: params.required ? 1 : 0, - img: { - type, - wmin: params.wmin || defaultMinWidth, - hmin: params.hmin || defaultMinHeight, - } - } : null; -} - -/** - * Produces a Native Data asset for the configuration given. - */ -function dataAsset(id, params, type, defaultLen) { - return params ? { - id, - required: params.required ? 1 : 0, - data: { - type, - len: params.len || defaultLen, - } - } : null; -} - -/** - * Produces an OpenRTB site object. - */ -function site(bidRequests, bidderRequest) { - const pubId = bidRequests && bidRequests.length > 0 ? bidRequests[0].params.cp : '0'; - const appParams = bidRequests[0].params.app; - if (!appParams) { - // use the first party data if available, and override only publisher/ref/page properties - var firstPartyData = bidderRequest?.ortb2?.site || {}; - return Object.assign({}, firstPartyData, { - publisher: { - id: pubId.toString(), - }, - // TODO: does the fallback make sense here? - ref: bidderRequest?.refererInfo?.ref || window.document.referrer, - page: bidderRequest?.refererInfo?.page || '' - }); - } - return null; -} - -/** - * Produces an OpenRTB App object. - */ -function app(bidderRequest) { - const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.cp : '0'; - const appParams = bidderRequest[0].params.app; - if (appParams) { - return { - publisher: { - id: pubId.toString(), - }, - bundle: appParams.bundle, - storeurl: appParams.storeUrl, - domain: appParams.domain, - } - } - return null; -} - -/** - * Produces an OpenRTB Device object. - */ -function device() { - return { - ua: navigator.userAgent, - language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), - }; -} - -/** - * Safely parses the input given. Returns null on - * parsing failure. - */ -function parse(rawResponse) { - try { - if (rawResponse) { - return JSON.parse(rawResponse); - } - } catch (ex) { - logError('pulsepointLite.safeParse', 'ERROR', ex); - } - return null; -} - -/** - * Determines the AdSize for the slot. - */ -function adSize(slot, sizes) { - if (slot.params.cf) { - const size = slot.params.cf.toUpperCase().split('X'); - const width = parseInt(slot.params.cw || size[0], 10); - const height = parseInt(slot.params.ch || size[1], 10); - return [width, height]; - } else if (sizes && sizes.length > 0) { - return [sizes[0].w, sizes[0].h]; - } - return [1, 1]; -} - -/** - * Handles the user level attributes and produces - * an openrtb User object. - */ -function user(bidRequest, bidderRequest) { - var user = bidderRequest?.ortb2?.user || { ext: {} }; - var ext = user.ext; - if (bidderRequest) { - if (bidderRequest.gdprConsent) { - ext.consent = bidderRequest.gdprConsent.consentString; - } - } - if (bidRequest) { - let eids = bidRequest.userIdAsEids; - if (eids) { - ext.eids = eids; - } - } - return user; -} - -/** - * Produces the regulations ortb object - */ -function regs(bidderRequest) { - if (bidderRequest.gdprConsent || bidderRequest.uspConsent) { - var ext = {}; - // GDPR applies attribute (actual consent value is in user object) - if (bidderRequest.gdprConsent) { - ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; - } - // CCPA - if (bidderRequest.uspConsent) { - ext.us_privacy = bidderRequest.uspConsent; - } - return { ext }; - } - return null; -} - -/** - * Creates source object with supply chain - */ -function source(schain) { - if (schain) { - return { - ext: { schain } - }; - } - return null; -} - -/** - * Parses the native response from the Bid given. - */ -function nativeResponse(imp, bid) { - if (imp['native']) { - const nativeAd = parse(bid.adm); - const keys = {}; - if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) { - nativeAd['native'].assets.forEach(asset => { - keys.title = asset.title ? asset.title.text : keys.title; - keys.body = asset.data && asset.data.type === 2 ? asset.data.value : keys.body; - keys.sponsoredBy = asset.data && asset.data.type === 1 ? asset.data.value : keys.sponsoredBy; - keys.image = asset.img && asset.img.type === 3 ? asset.img.url : keys.image; - keys.icon = asset.img && asset.img.type === 1 ? asset.img.url : keys.icon; - }); - if (nativeAd['native'].link) { - keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url); - } - keys.impressionTrackers = nativeAd['native'].imptrackers; - return keys; - } - } - return null; -} - -function bidFloor(slot) { - let floor = slot.params.bidfloor; - if (isFn(slot.getFloor)) { - const floorData = slot.getFloor({ - mediaType: slot.mediaTypes.banner ? 'banner' : slot.mediaTypes.video ? 'video' : 'Native', - size: '*', - currency: DEFAULT_CURRENCY, - }); - if (floorData && floorData.floor) { - floor = floorData.floor; - } - } - return floor; -} - registerBidder(spec); diff --git a/modules/pulsepointBidAdapter.md b/modules/pulsepointBidAdapter.md index 7f4b7e6b611..899c277f92f 100644 --- a/modules/pulsepointBidAdapter.md +++ b/modules/pulsepointBidAdapter.md @@ -2,7 +2,7 @@ **Module Name**: PulsePoint Bidder Adapter **Module Type**: Bidder Adapter -**Maintainer**: ExchangeTeam@pulsepoint.com +**Maintainer**: ExchangeTeam@pulsepoint.com # Description @@ -18,55 +18,49 @@ Please use ```pulsepoint``` as the bidder code. sizes: [[300, 250]], bids: [{ bidder: 'pulsepoint', - params: { - cf: '300X250', + params: { cp: 512379, ct: 486653 } }] },{ - code: 'native-ad-div', - sizes: [[1, 1]], - nativeParams: { - title: { required: true, len: 75 }, - image: { required: true }, - body: { len: 200 }, - sponsoredBy: { len: 20 } - }, - bids: [{ - bidder: 'pulsepoint', - params: { - cp: 512379, - ct: 505642 - } - }] - },{ - code: 'outstream-div', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream', - h: 300, - w: 400, - minduration: 1, - maxduration: 210, - linearity: 1, - mimes: ["video/mp4", "video/ogg", "video/webm"], - pos: 3 - } - }, - bids: [{ - bidder: 'pulsepoint', - params: { - cp: 512379, - ct: 505642 - } - }], - renderer: { - options: { - text: "PulsePoint Outstream" - } - } + code: 'native-1-slot', + mediaTypes: { + native: { + ortb: { + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 150, + h: 50, + } + }, + { + id: 2, + required: 1, + title: { + len: 80 + } + }, + { + id: 3, + required: 1, + data: { + type: 1 + } + }] + } + } + }, + bids: [{ + bidder: 'pulsepoint', + params: { + cp: 512379, + ct: 694973 + } + }] },{ code: 'instream', mediaTypes: { diff --git a/modules/qortexRtdProvider.js b/modules/qortexRtdProvider.js new file mode 100644 index 00000000000..7aa30334756 --- /dev/null +++ b/modules/qortexRtdProvider.js @@ -0,0 +1,165 @@ +import { submodule } from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { logWarn, mergeDeep, logMessage, generateUUID } from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; + +let requestUrl; +let bidderArray; +let impressionIds; +let currentSiteContext; + +/** + * Init if module configuration is valid + * @param {Object} config Module configuration + * @returns {Boolean} + */ +function init (config) { + if (!config?.params?.groupId?.length > 0) { + logWarn('Qortex RTD module config does not contain valid groupId parameter. Config params: ' + JSON.stringify(config.params)) + return false; + } else { + initializeModuleData(config); + } + if (config?.params?.tagConfig) { + loadScriptTag(config) + } + return true; +} + +/** + * Processess prebid request and attempts to add context to ort2b fragments + * @param {Object} reqBidsConfig Bid request configuration object + * @param {Function} callback Called on completion + */ +function getBidRequestData (reqBidsConfig, callback) { + if (reqBidsConfig?.adUnits?.length > 0) { + getContext() + .then(contextData => { + setContextData(contextData) + addContextToRequests(reqBidsConfig) + callback(); + }) + .catch((e) => { + logWarn(e?.message); + callback(); + }); + } else { + logWarn('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfig)) + callback(); + } +} + +/** + * determines whether to send a request to context api and does so if necessary + * @returns {Promise} ortb Content object + */ +export function getContext () { + if (!currentSiteContext) { + logMessage('Requesting new context data'); + return new Promise((resolve, reject) => { + const callbacks = { + success(text, data) { + const result = data.status === 200 ? JSON.parse(data.response)?.content : null; + resolve(result); + }, + error(error) { + reject(new Error(error)); + } + } + ajax(requestUrl, callbacks) + }) + } else { + logMessage('Adding Content object from existing context data'); + return new Promise(resolve => resolve(currentSiteContext)); + } +} + +/** + * Updates bidder configs with the response from Qortex context services + * @param {Object} reqBidsConfig Bid request configuration object + * @param {string[]} bidders Bidders specified in module's configuration + */ +export function addContextToRequests (reqBidsConfig) { + if (currentSiteContext === null) { + logWarn('No context data received at this time'); + } else { + const fragment = { site: {content: currentSiteContext} } + if (bidderArray?.length > 0) { + bidderArray.forEach(bidder => mergeDeep(reqBidsConfig.ortb2Fragments.bidder, {[bidder]: fragment})) + } else if (!bidderArray) { + mergeDeep(reqBidsConfig.ortb2Fragments.global, fragment); + } else { + logWarn('Config contains an empty bidders array, unable to determine which bids to enrich'); + } + } +} + +/** + * Loads Qortex header tag using data passed from module config object + * @param {Object} config module config obtained during init + */ +export function loadScriptTag(config) { + const code = 'qortex'; + const groupId = config.params.groupId; + const src = 'https://tags.qortex.ai/bootstrapper' + const attr = {'data-group-id': groupId} + const tc = config.params.tagConfig + + Object.keys(tc).forEach(p => { + attr[`data-${p.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`)}`] = tc[p] + }) + + addEventListener('qortex-rtd', (e) => { + const billableEvent = { + vendor: code, + billingId: generateUUID(), + type: e?.detail?.type, + accountId: groupId + } + switch (e?.detail?.type) { + case 'qx-impression': + const {uid} = e.detail; + if (!uid || impressionIds.has(uid)) { + logWarn(`received invalid billable event due to ${!uid ? 'missing' : 'duplicate'} uid: qx-impression`) + return; + } else { + logMessage('received billable event: qx-impression') + impressionIds.add(uid) + billableEvent.transactionId = e.detail.uid; + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, billableEvent); + break; + } + default: + logWarn(`received invalid billable event: ${e.detail?.type}`) + } + }) + + loadExternalScript(src, code, undefined, undefined, attr); +} + +/** + * Helper function to set initial values when they are obtained by init + * @param {Object} config module config obtained during init + */ +export function initializeModuleData(config) { + const DEFAULT_API_URL = 'https://demand.qortex.ai'; + const {apiUrl, groupId, bidders} = config.params; + requestUrl = `${apiUrl || DEFAULT_API_URL}/api/v1/analyze/${groupId}/prebid`; + bidderArray = bidders; + impressionIds = new Set(); + currentSiteContext = null; +} + +export function setContextData(value) { + currentSiteContext = value +} + +export const qortexSubmodule = { + name: 'qortex', + init, + getBidRequestData +} + +submodule('realTimeData', qortexSubmodule); diff --git a/modules/qortexRtdProvider.md b/modules/qortexRtdProvider.md new file mode 100644 index 00000000000..312696068cd --- /dev/null +++ b/modules/qortexRtdProvider.md @@ -0,0 +1,69 @@ +# Qortex Real-time Data Submodule + +## Overview + +``` +Module Name: Qortex RTD Provider +Module Type: RTD Provider +Maintainer: mannese@qortex.ai +``` + +## Description + +The Qortex RTD module appends contextual segments to the bidding object based on the content of a page using the Qortex API. + +Upon load, the Qortex context API will analyze the bidder page (video, text, image, etc.) and will return a [Content object](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=26). The module will then merge that object into the appropriate bidders' `ortb2.site.content`, which can be used by prebid adapters that use `site.content` data. + + +## Build +``` +gulp build --modules="rtdModule,qortexRtdProvider,qortexBidAdapter,..." +``` + +> `rtdModule` is a required module to use Qortex RTD module. + +## Configuration + +Please refer to [Prebid Documentation](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-realTimeData) on RTD module configuration for details on required and optional parameters of `realTimeData` + +When configuring Qortex as a data provider, refer to the template below to add the necessary information to ensure the proper connection is made. + +### RTD Module Setup + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 1000, + dataProviders: [{ + name: 'qortex', + waitForIt: true, + params: { + groupId: 'ABC123', //required + bidders: ['qortex', 'adapter2'], //optional (see below) + tagConfig: { // optional, please reach out to your account manager for configuration reccommendation + videoContainer: 'string', + htmlContainer: 'string', + attachToTop: 'string', + esm6Mod: 'string', + continuousLoad: 'string' + } + } + }] + } +}); +``` + +### Paramter Details + +#### `groupId` - Required +- The Qortex groupId linked to the publisher, this is required to make a request using this adapter + +#### `bidders` - optional +- If this parameter is included, it must be an array of the strings that match the bidder code of the prebid adapters you would like this module to impact. `ortb2.site.content` will be updated *only* for adapters in this array + +- If this parameter is omitted, the RTD module will default to updating `ortb2.site.content` on *all* bid adapters being used on the page + +#### `tagConfig` - optional +- This optional parameter is an object containing the config settings that could be usedto initialize the Qortex integration on your page. A preconfigured object for this step will be provided to you by the Qortex team. + +- If this parameter is not present, the Qortex integration can still be configured and loaded manually on your page outside of prebid. The RTD module will continue to initialize and operate as normal. \ No newline at end of file diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index 6a07082b61c..2faf638fc0b 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -214,6 +214,12 @@ export const quantcastIdSubmodule = { } return { id: fpa ? { quantcastId: fpa } : undefined }; + }, + eids: { + 'quantcastId': { + source: 'quantcast.com', + atype: 1 + }, } }; diff --git a/modules/rasBidAdapter.js b/modules/rasBidAdapter.js index a7aceb107b9..4e93f2aa8eb 100644 --- a/modules/rasBidAdapter.js +++ b/modules/rasBidAdapter.js @@ -1,7 +1,8 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -import { isEmpty, getAdUnitSizes, parseSizesInput, deepAccess } from '../src/utils.js'; +import { isEmpty, parseSizesInput, deepAccess } from '../src/utils.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'ras'; const VERSION = '1.0'; @@ -129,6 +130,36 @@ const getGdprParams = (bidderRequest) => { return queryString; }; +const parseAuctionConfigs = (serverResponse, bidRequest) => { + if (isEmpty(bidRequest)) { + return null; + } + const auctionConfigs = []; + const gctx = serverResponse && serverResponse.body?.gctx; + + bidRequest.bidIds.filter(bid => bid.fledgeEnabled).forEach((bid) => { + auctionConfigs.push({ + 'bidId': bid.bidId, + 'config': { + 'seller': 'https://csr.onet.pl', + 'decisionLogicUrl': `https://csr.onet.pl/${encodeURIComponent(bid.params.network)}/v1/protected-audience-api/decision-logic.js`, + 'interestGroupBuyers': ['https://csr.onet.pl'], + 'auctionSignals': { + 'params': bid.params, + 'sizes': bid.sizes, + 'gctx': gctx + } + } + }); + }); + + if (auctionConfigs.length === 0) { + return null; + } else { + return auctionConfigs; + } +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], @@ -145,8 +176,16 @@ export const spec = { const slotsQuery = getSlots(bidRequests); const contextQuery = getContextParams(bidRequests, bidderRequest); const gdprQuery = getGdprParams(bidderRequest); - const bidIds = bidRequests.map((bid) => ({ slot: bid.params.slot, bidId: bid.bidId })); + const fledgeEligible = Boolean(bidderRequest && bidderRequest.fledgeEnabled); const network = bidRequests[0].params.network; + const bidIds = bidRequests.map((bid) => ({ + slot: bid.params.slot, + bidId: bid.bidId, + sizes: getAdUnitSizes(bid), + params: bid.params, + fledgeEnabled: fledgeEligible + })); + return [{ method: 'GET', url: getEndpoint(network) + contextQuery + slotsQuery + gdprQuery, @@ -156,10 +195,16 @@ export const spec = { interpretResponse: function (serverResponse, bidRequest) { const response = serverResponse.body; - if (!response || !response.ads || response.ads.length === 0) { - return []; + + const fledgeAuctionConfigs = parseAuctionConfigs(serverResponse, bidRequest); + const bids = (!response || !response.ads || response.ads.length === 0) ? [] : response.ads.map(buildBid).filter((bid) => !isEmpty(bid)); + + if (fledgeAuctionConfigs) { + // Return a tuple of bids and auctionConfigs. It is possible that bids could be null. + return {bids, fledgeAuctionConfigs}; + } else { + return bids; } - return response.ads.map(buildBid).filter((bid) => !isEmpty(bid)); } }; diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js index b2961b09eb5..1e702d812f0 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -1,4 +1,14 @@ -import { deepAccess, logWarn, getBidIdParameter, parseQueryStringParameters, triggerPixel, generateUUID, isArray, isNumber, parseSizesInput } from '../src/utils.js'; +import { + deepAccess, + logWarn, + parseQueryStringParameters, + triggerPixel, + generateUUID, + isArray, + isNumber, + parseSizesInput, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { Renderer } from '../src/Renderer.js'; diff --git a/modules/relayBidAdapter.js b/modules/relayBidAdapter.js new file mode 100644 index 00000000000..af145a5e163 --- /dev/null +++ b/modules/relayBidAdapter.js @@ -0,0 +1,99 @@ +import { isNumber, logMessage } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' + +const BIDDER_CODE = 'relay'; +const METHOD = 'POST'; +const ENDPOINT_URL = 'https://e.relay.bid/p/openrtb2'; + +// The default impl from the prebid docs. +const CONVERTER = + ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + } + }); + +function buildRequests(bidRequests, bidderRequest) { + const prebidVersion = config.getConfig('prebid_version') || 'v8.1.0'; + // Group bids by accountId param + const groupedByAccountId = bidRequests.reduce((accu, item) => { + const accountId = ((item || {}).params || {}).accountId; + if (!accu[accountId]) { accu[accountId] = []; }; + accu[accountId].push(item); + return accu; + }, {}); + // Send one overall request with all grouped bids per accountId + let reqs = []; + for (const [accountId, accountBidRequests] of Object.entries(groupedByAccountId)) { + const url = `${ENDPOINT_URL}?a=${accountId}&pb=1&pbv=${prebidVersion}`; + const data = CONVERTER.toORTB({ bidRequests: accountBidRequests, bidderRequest }) + const req = { + method: METHOD, + url, + data + }; + reqs.push(req); + } + return reqs; +}; + +function interpretResponse(response, request) { + return CONVERTER.fromORTB({ response: response.body, request: request.data }).bids; +}; + +function isBidRequestValid(bid) { + return isNumber((bid.params || {}).accountId); +}; + +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncs = [] + for (const response of serverResponses) { + const responseSyncs = ((((response || {}).body || {}).ext || {}).user_syncs || []) + // Relay returns user_syncs in the format expected by prebid. If for any + // reason the request/response failed to properly capture the GDPR settings + // -- fallback to those identified by Prebid. + for (const sync of responseSyncs) { + const syncUrl = new URL(sync.url); + const missingGdpr = !syncUrl.searchParams.has('gdpr'); + const missingGdprConsent = !syncUrl.searchParams.has('gdpr_consent'); + if (missingGdpr) { + syncUrl.searchParams.set('gdpr', Number(gdprConsent.gdprApplies)) + sync.url = syncUrl.toString(); + } + if (missingGdprConsent) { + syncUrl.searchParams.set('gdpr_consent', gdprConsent.consentString); + sync.url = syncUrl.toString(); + } + if (syncOptions.iframeEnabled && sync.type === 'iframe') { + syncs.push(sync); + } else if (syncOptions.pixelEnabled && sync.type === 'image') { + syncs.push(sync); + } + } + } + + return syncs; +} + +export const spec = { + code: BIDDER_CODE, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + onTimeout: function (timeoutData) { + logMessage('Timeout: ', timeoutData); + }, + onBidWon: function (bid) { + logMessage('Bid won: ', bid); + }, + onBidderError: function ({ error, bidderRequest }) { + logMessage('Error: ', error, bidderRequest); + }, + supportedMediaTypes: [BANNER, VIDEO, NATIVE] +} +registerBidder(spec); diff --git a/modules/relayBidAdapter.md b/modules/relayBidAdapter.md new file mode 100644 index 00000000000..882e04b7b13 --- /dev/null +++ b/modules/relayBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Relay Bid Adapter +Module Type: Bid Adapter +Maintainer: relay@kevel.co +``` + +# Description + +Connects to Relay exchange API for bids. +Supports Banner, Video and Native. + +# Test Parameters + +``` +var adUnits = [ + // Banner with minimal bid configuration + { + code: 'minimal', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'relay', + params: { + accountId: 1234 + }, + ortb2imp: { + ext: { + relay: { + bidders: { + bidderA: { + param: 1234 + } + } + } + } + } + } + ] + }, + // Minimal video + { + code: 'video-minimal', + mediaTypes: { + video: { + maxduration: 30, + api: [1, 3], + mimes: ['video/mp4'], + placement: 3, + protocols: [2,3,5,6] + } + }, + bids: [ + { + bidder: 'relay', + params: { + accountId: 1234 + }, + ortb2imp: { + ext: { + relay: { + bidders: { + bidderA: { + param: 'example' + } + } + } + } + } + } + ] + } +]; +``` diff --git a/modules/relevadRtdProvider.js b/modules/relevadRtdProvider.js index a7d0305da62..613eaa71a1f 100644 --- a/modules/relevadRtdProvider.js +++ b/modules/relevadRtdProvider.js @@ -110,6 +110,7 @@ function composeOrtb2Data(rtdData, prefix) { !isEmpty(categories.cat) && deepSetValue(addOrtb2, prefix + '.cat', categories.cat); !isEmpty(categories.pagecat) && deepSetValue(addOrtb2, prefix + '.pagecat', categories.pagecat); !isEmpty(categories.sectioncat) && deepSetValue(addOrtb2, prefix + '.sectioncat', categories.sectioncat); + !isEmpty(categories.sectioncat) && deepSetValue(addOrtb2, prefix + '.ext.data.relevad_rtd', categories.sectioncat); !isEmpty(categories.cattax) && deepSetValue(addOrtb2, prefix + '.cattax', categories.cattax); if (!isEmpty(content) && !isEmpty(content.segs) && content.segtax) { @@ -134,7 +135,8 @@ function setBidderSiteAndContent(bidderOrtbFragment, bidder, rtdData) { try { let addOrtb2 = composeOrtb2Data(rtdData, 'site'); !isEmpty(rtdData.segments) && deepSetValue(addOrtb2, 'user.ext.data.relevad_rtd', rtdData.segments); - !isEmpty(rtdData.categories?.sectioncat) && deepSetValue(addOrtb2, 'site.ext.data.relevad_rtd', rtdData.categories.sectioncat); + !isEmpty(rtdData.segments) && deepSetValue(addOrtb2, 'user.ext.data.segments', rtdData.segments); + !isEmpty(rtdData.categories) && deepSetValue(addOrtb2, 'user.ext.data.contextual_categories', rtdData.categories.pagecat); if (isEmpty(addOrtb2)) { return; } @@ -180,7 +182,7 @@ function getFiltered(data, minscore) { const cats = filterByScore(data.cats, minscore); const pcats = filterByScore(data.pcats, minscore) || cats; const scats = filterByScore(data.scats, minscore) || pcats; - const cattax = data.cattax ? data.cattax : CATTAX_IAB; + const cattax = (data.cattax || data.cattax === undefined) ? data.cattax : CATTAX_IAB; relevadData.categories = {cat: cats, pagecat: pcats, sectioncat: scats, cattax: cattax}; const contsegs = filterByScore(data.contsegs, minscore); @@ -268,15 +270,12 @@ export function addRtdData(reqBids, data, moduleConfig) { } if (wb && !isEmpty(relevadList)) { setBidderSiteAndContent(reqBids.ortb2Fragments?.bidder, bid.bidder, relevadData); + setBidderSiteAndContent(bid, 'ortb2', relevadData); deepSetValue(bid, 'params.keywords.relevad_rtd', relevadList); - deepSetValue(bid, 'params.target', [].concat(bid.params?.target ? [bid.params.target] : []).concat(relevadList.map(entry => { return 'relevad_rtd=' + entry; })).join(';')); + !(bid.params?.target || '').includes('relevad_rtd=') && deepSetValue(bid, 'params.target', [].concat(bid.params?.target ? [bid.params.target] : []).concat(relevadList.map(entry => { return 'relevad_rtd=' + entry; })).join(';')); let firstPartyData = {}; firstPartyData[bid.bidder] = { firstPartyData: { relevad_rtd: relevadList } }; config.setConfig(firstPartyData); - !isEmpty(relevadData.segments) && deepSetValue(bid, 'ortb2.user.ext.data.segments', relevadData.segments); - !isEmpty(relevadData.categories) && deepSetValue(bid, 'ortb2.user.ext.data.contextual_categories', relevadData.categories.pagecat); - !isEmpty(relevadData.categories) && deepSetValue(bid, 'ortb2.site.ext.data.relevad_rtd', relevadData.categories.pagecat); - !isEmpty(relevadData.segments) && deepSetValue(bid, 'ortb2.user.ext.data.relevad_rtd', relevadData.segments); } } } catch (e) { diff --git a/modules/relevantdigitalBidAdapter.js b/modules/relevantdigitalBidAdapter.js index ad9ee5e1e14..ef4e1c8e33d 100644 --- a/modules/relevantdigitalBidAdapter.js +++ b/modules/relevantdigitalBidAdapter.js @@ -98,7 +98,14 @@ export const spec = { isBidRequestValid: (bid) => bid.params?.placementId && getBidderConfig([bid]).complete, /** Trigger impression-pixel */ - onBidWon: ({pbsWurl}) => pbsWurl && triggerPixel(pbsWurl), + onBidWon(bid) { + if (bid.pbsWurl) { + triggerPixel(bid.pbsWurl) + } + if (bid.burl) { + triggerPixel(bid.burl) + } + }, /** Build BidRequest for PBS */ buildRequests(bidRequests, bidderRequest) { @@ -193,6 +200,24 @@ export const spec = { }); return syncs; }, + + /** If server side, transform bid params if needed */ + transformBidParams(params, isOrtb, adUnit, bidRequests) { + if (!params.placementId) { + return; + } + const bid = bidRequests.flatMap(req => req.adUnitsS2SCopy || []).flatMap((adUnit) => adUnit.bids).find((bid) => bid.params?.placementId === params.placementId); + if (!bid) { + return; + } + const cfg = getBidderConfig([bid]); + FIELDS.forEach(({ name }) => { + if (cfg[name] && !params[name]) { + params[name] = cfg[name]; + } + }); + return params; + }, }; registerBidder(spec); diff --git a/modules/resetdigitalBidAdapter.js b/modules/resetdigitalBidAdapter.js index 8264e0cc9cc..2bac6c6dcba 100644 --- a/modules/resetdigitalBidAdapter.js +++ b/modules/resetdigitalBidAdapter.js @@ -1,25 +1,29 @@ import { timestamp, deepAccess, isStr, deepClone } from '../src/utils.js'; import { getOrigin } from '../libraries/getOrigin/index.js'; import { config } from '../src/config.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; const BIDDER_CODE = 'resetdigital'; const CURRENCY = 'USD'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [ 'banner', 'video' ], - isBidRequestValid: function(bid) { - return (!!(bid.params.pubId || bid.params.zoneId)); + supportedMediaTypes: ['banner', 'video'], + isBidRequestValid: function (bid) { + return !!(bid.params.pubId || bid.params.zoneId); }, - buildRequests: function(validBidRequests, bidderRequest) { - let stack = (bidderRequest.refererInfo && - bidderRequest.refererInfo.stack ? bidderRequest.refererInfo.stack - : []) - - let spb = (config.getConfig('userSync') && config.getConfig('userSync').syncsPerBidder) - ? config.getConfig('userSync').syncsPerBidder : 5 + buildRequests: function (validBidRequests, bidderRequest) { + let stack = + bidderRequest.refererInfo && bidderRequest.refererInfo.stack + ? bidderRequest.refererInfo.stack + : []; + + let spb = + config.getConfig('userSync') && + config.getConfig('userSync').syncsPerBidder + ? config.getConfig('userSync').syncsPerBidder + : 5; const payload = { start_time: timestamp(), @@ -29,19 +33,19 @@ export const spec = { iframe: !bidderRequest.refererInfo.reachedTop, // TODO: the last element in refererInfo.stack is window.location.href, that's unlikely to have been the intent here url: stack && stack.length > 0 ? [stack.length - 1] : null, - https: (window.location.protocol === 'https:'), + https: window.location.protocol === 'https:', // TODO: is 'page' the right value here? - referrer: bidderRequest.refererInfo.page + referrer: bidderRequest.refererInfo.page, }, imps: [], user_ids: validBidRequests[0].userId, - sync_limit: spb + sync_limit: spb, }; if (bidderRequest && bidderRequest.gdprConsent) { payload.gdpr = { applies: bidderRequest.gdprConsent.gdprApplies, - consent: bidderRequest.gdprConsent.consentString + consent: bidderRequest.gdprConsent.consentString, }; } @@ -50,10 +54,16 @@ export const spec = { } function getOrtb2Keywords(ortb2Obj) { - const fields = ['site.keywords', 'site.content.keywords', 'user.keywords', 'app.keywords', 'app.content.keywords']; + const fields = [ + 'site.keywords', + 'site.content.keywords', + 'user.keywords', + 'app.keywords', + 'app.content.keywords', + ]; let result = []; - fields.forEach(path => { + fields.forEach((path) => { let keyStr = deepAccess(ortb2Obj, path); if (isStr(keyStr)) result.push(keyStr); }); @@ -79,18 +89,26 @@ export const spec = { const floorInfo = req.getFloor({ currency: CURRENCY, mediaType: BANNER, - size: '*' + size: '*', }); - if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + if ( + typeof floorInfo === 'object' && + floorInfo.currency === CURRENCY && + !isNaN(parseFloat(floorInfo.floor)) + ) { bidFloor = parseFloat(floorInfo.floor); bidFloorCur = CURRENCY; } } // get param kewords (if it exists) - let paramsKeywords = req.params.keywords ? req.params.keywords.split(',') : []; + let paramsKeywords = req.params.keywords + ? req.params.keywords.split(',') + : []; // merge all keywords - let keywords = ortb2KeywordsList.concat(paramsKeywords).concat(metaKeywords); + let keywords = ortb2KeywordsList + .concat(paramsKeywords) + .concat(metaKeywords); payload.imps.push({ pub_id: req.params.pubId, @@ -110,32 +128,32 @@ export const spec = { sizes: req.sizes, force_bid: req.params.forceBid, coppa: config.getConfig('coppa') === true ? 1 : 0, - media_types: deepAccess(req, 'mediaTypes') + media_types: deepAccess(req, 'mediaTypes'), }); } - let params = validBidRequests[0].params - let url = params.endpoint ? params.endpoint : '//ads.resetsrv.com' + let params = validBidRequests[0].params; + let url = params.endpoint ? params.endpoint : '//ads.resetsrv.com'; return { method: 'POST', url: url, data: JSON.stringify(payload), - bids: validBidRequests + bids: validBidRequests, }; }, - interpretResponse: function(serverResponse, bidRequest) { + interpretResponse: function (serverResponse, bidRequest) { const bidResponses = []; if (!serverResponse || !serverResponse.body) { - return bidResponses + return bidResponses; } let res = serverResponse.body; if (!res.bids || !res.bids.length) { - return [] + return []; } for (let x = 0; x < serverResponse.body.bids.length; x++) { - let bid = serverResponse.body.bids[x] + let bid = serverResponse.body.bids[x]; bidResponses.push({ requestId: bid.bid_id, @@ -152,47 +170,45 @@ export const spec = { netRevenue: true, currency: 'USD', meta: { - advertiserDomains: bid.adomain - } - }) + advertiserDomains: bid.adomain, + }, + }); } return bidResponses; }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { - const syncs = [] + getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { + const syncs = []; if (!serverResponses.length || !serverResponses[0].body) { - return syncs + return syncs; } - let pixels = serverResponses[0].body.pixels + let pixels = serverResponses[0].body.pixels; if (!pixels || !pixels.length) { - return syncs + return syncs; } - let gdprParams = null + let gdprParams = ''; if (gdprConsent) { if (typeof gdprConsent.gdprApplies === 'boolean') { - gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}` + gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${ + gdprConsent.consentString + }`; } else { - gdprParams = `gdpr_consent=${gdprConsent.consentString}` + gdprParams = `gdpr_consent=${gdprConsent.consentString}`; } } - for (let x = 0; x < pixels.length; x++) { - let pixel = pixels[x] - - if ((pixel.type === 'iframe' && syncOptions.iframeEnabled) || - (pixel.type === 'image' && syncOptions.pixelEnabled)) { - if (gdprParams && gdprParams.length) { - pixel = (pixel.indexOf('?') === -1 ? '?' : '&') + gdprParams - } - syncs.push(pixel) - } + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { + return [ + { + type: 'iframe', + url: 'https://media.reset-digital.com/prebid/async_usersync.html?' + gdprParams.length ? gdprParams : '', + }, + ]; } - return syncs; - } + }, }; registerBidder(spec); diff --git a/modules/revcontentBidAdapter.js b/modules/revcontentBidAdapter.js index 5bf7dd691e7..f1d5521f780 100644 --- a/modules/revcontentBidAdapter.js +++ b/modules/revcontentBidAdapter.js @@ -3,9 +3,10 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; -import {_map, deepAccess, getAdUnitSizes, isFn, parseGPTSingleSizeArrayToRtbSize, triggerPixel} from '../src/utils.js'; +import {_map, deepAccess, isFn, parseGPTSingleSizeArrayToRtbSize, triggerPixel} from '../src/utils.js'; import {parseDomain} from '../src/refererDetection.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'revcontent'; const NATIVE_PARAMS = { diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index d5c6469db12..78740f7f87d 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 5fad246f27d..bff7661455a 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -7,7 +7,6 @@ import { find } from '../src/polyfill.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { Renderer } from '../src/Renderer.js'; import { - convertTypes, deepAccess, deepSetValue, formatQS, @@ -18,16 +17,19 @@ import { logMessage, logWarn, mergeDeep, - parseSizesInput, _each + parseSizesInput, + pick, + _each } from '../src/utils.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const DEFAULT_INTEGRATION = 'pbjs_lite'; const DEFAULT_PBS_INTEGRATION = 'pbjs'; const DEFAULT_RENDERER_URL = 'https://video-outstream.rubiconproject.com/apex-2.2.1.js'; // renderer code at https://github.com/rubicon-project/apex2 -let rubiConf = {}; +let rubiConf = config.getConfig('rubicon') || {}; // we are saving these as global to this module so that if a pub accidentally overwrites the entire // rubicon object, then we do not lose other data config.getConfig('rubicon', config => { @@ -124,6 +126,7 @@ var sizeMap = { 278: '320x500', 282: '320x400', 288: '640x380', + 484: '720x1280', 524: '1x2', 548: '500x1000', 550: '980x480', @@ -139,7 +142,9 @@ var sizeMap = { 576: '610x877', 578: '980x552', 580: '505x656', - 622: '192x160' + 622: '192x160', + 632: '1200x450', + 634: '340x450' }; _each(sizeMap, (item, key) => sizeMap[item] = key); @@ -196,10 +201,14 @@ export const converter = ortbConverter({ if (config.getConfig('s2sConfig.defaultTtl')) { imp.exp = config.getConfig('s2sConfig.defaultTtl'); }; - bidRequest.params.position === 'atf' && (imp.video.pos = 1); - bidRequest.params.position === 'btf' && (imp.video.pos = 3); + bidRequest.params.position === 'atf' && imp.video && (imp.video.pos = 1); + bidRequest.params.position === 'btf' && imp.video && (imp.video.pos = 3); delete imp.ext?.prebid?.storedrequest; + if (bidRequest.params.bidonmultiformat === true && bidRequestType.length > 1) { + deepSetValue(imp, 'ext.prebid.bidder.rubicon.formats', bidRequestType); + } + setBidFloors(bidRequest, imp); return imp; @@ -318,7 +327,7 @@ export const spec = { ) ); }); - if (config.getConfig('rubicon.singleRequest') !== true) { + if (rubiConf.singleRequest !== true) { // bids are not grouped if single request mode is not enabled requests = filteredHttpRequest.concat(bannerBidRequests.map(bidRequest => { const bidParams = spec.createSlotParams(bidRequest, bidderRequest); @@ -383,6 +392,8 @@ export const spec = { 'gdpr', 'gdpr_consent', 'us_privacy', + 'gpp', + 'gpp_sid', 'rp_schain', ].concat(Object.keys(params).filter(item => containsUId.test(item))) .concat([ @@ -399,6 +410,8 @@ export const spec = { 'x_source.tid', 'l_pb_bid_id', 'p_screen_res', + 'o_ae', + 'o_cdep', 'rp_floor', 'rp_secure', 'tk_user_key' @@ -472,6 +485,7 @@ export const spec = { 'x_source.tid': bidderRequest.ortb2?.source?.tid, 'x_imp.ext.tid': bidRequest.ortb2Imp?.ext?.tid, 'l_pb_bid_id': bidRequest.bidId, + 'o_cdep': bidRequest.ortb2?.device?.ext?.cdep, 'p_screen_res': _getScreenResolution(), 'tk_user_key': params.userId, 'p_geo.latitude': isNaN(parseFloat(latitude)) ? undefined : parseFloat(latitude).toFixed(4), @@ -495,6 +509,11 @@ export const spec = { data['rp_hard_floor'] = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; } + // Send multiformat data if requested + if (params.bidonmultiformat === true && deepAccess(bidRequest, 'mediaTypes') && Object.keys(bidRequest.mediaTypes).length > 1) { + data['p_formats'] = Object.keys(bidRequest.mediaTypes).join(','); + } + // add p_pos only if specified and valid // For SRA we need to explicitly put empty semi colons so AE treats it as empty, instead of copying the latter value let posMapping = {1: 'atf', 3: 'btf'}; @@ -506,6 +525,10 @@ export const spec = { if (configUserId) { data['ppuid'] = configUserId; } + + if (bidRequest?.ortb2Imp?.ext?.ae) { + data['o_ae'] = 1; + } // loop through userIds and add to request if (bidRequest.userIdAsEids) { bidRequest.userIdAsEids.forEach(eid => { @@ -526,7 +549,9 @@ export const spec = { data['eid_id5-sync.com'] = `${eid.uids[0].id}^${eid.uids[0].atype}^${(eid.uids[0].ext && eid.uids[0].ext.linkType) || ''}`; } else { // add anything else with this generic format - data[`eid_${eid.source}`] = `${eid.uids[0].id}^${eid.uids[0].atype || ''}`; + // if rubicon drop ^ + const id = eid.source === 'rubiconproject.com' ? eid.uids[0].id : `${eid.uids[0].id}^${eid.uids[0].atype || ''}` + data[`eid_${eid.source}`] = id; } // send AE "ppuid" signal if exists, and hasn't already been sent if (!data['ppuid']) { @@ -554,6 +579,11 @@ export const spec = { data['us_privacy'] = encodeURIComponent(bidderRequest.uspConsent); } + if (bidderRequest.gppConsent?.gppString) { + data['gpp'] = bidderRequest.gppConsent.gppString; + data['gpp_sid'] = bidderRequest.gppConsent?.applicableSections?.toString(); + } + data['rp_maxbids'] = bidderRequest.bidLimit || 1; applyFPD(bidRequest, BANNER, data); @@ -598,7 +628,7 @@ export const spec = { * @param {*} responseObj * @param {BidRequest|Object.} request - if request was SRA the bidRequest argument will be a keyed BidRequest array object, * non-SRA responses return a plain BidRequest object - * @return {Bid[]} An array of bids which + * @return {{fledgeAuctionConfigs: *, bids: *}} An array of bids which */ interpretResponse: function (responseObj, request) { responseObj = responseObj.body; @@ -608,7 +638,6 @@ export const spec = { if (!responseObj || typeof responseObj !== 'object') { return []; } - // Response from PBS Java openRTB if (responseObj.seatbid) { const responseErrors = deepAccess(responseObj, 'ext.errors.rubicon'); @@ -634,7 +663,7 @@ export const spec = { return []; } - return ads.reduce((bids, ad, i) => { + let bids = ads.reduce((bids, ad, i) => { (ad.impression_id && lastImpId === ad.impression_id) ? multibid++ : lastImpId = ad.impression_id; if (ad.status !== 'ok') { @@ -695,8 +724,18 @@ export const spec = { }, []).sort((adA, adB) => { return (adB.cpm || 0.0) - (adA.cpm || 0.0); }); + + let fledgeAuctionConfigs = responseObj.component_auction_config?.map(config => { + return { config, bidId: config.bidId } + }); + + if (fledgeAuctionConfigs) { + return { bids, fledgeAuctionConfigs }; + } else { + return bids; + } }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { if (!hasSynced && syncOptions.iframeEnabled) { // data is only assigned if params are available to pass to syncEndpoint let params = {}; @@ -714,6 +753,11 @@ export const spec = { params['us_privacy'] = encodeURIComponent(uspConsent); } + if (gppConsent?.gppString) { + params['gpp'] = gppConsent.gppString; + params['gpp_sid'] = gppConsent.applicableSections?.toString(); + } + params = Object.keys(params).length ? `?${formatQS(params)}` : ''; hasSynced = true; @@ -931,6 +975,33 @@ function applyFPD(bidRequest, mediaType, data) { if (data['tg_i.pbadslot']) { delete data['tg_i.dfp_ad_unit_code']; } + + // High Entropy stuff -> sua object is the ORTB standard (default to pass unless specifically disabled) + const clientHints = deepAccess(fpd, 'device.sua'); + if (clientHints && rubiConf.chEnabled !== false) { + // pick out client hints we want to send (any that are undefined or empty will NOT be sent) + pick(clientHints, [ + 'architecture', arch => data.m_ch_arch = arch, + 'bitness', bitness => data.m_ch_bitness = bitness, + 'browsers', browsers => { + if (!Array.isArray(browsers)) return; + // reduce down into ua and full version list attributes + const [ua, fullVer] = browsers.reduce((accum, browserData) => { + accum[0].push(`"${browserData?.brand}"|v="${browserData?.version?.[0]}"`); + accum[1].push(`"${browserData?.brand}"|v="${browserData?.version?.join?.('.')}"`); + return accum; + }, [[], []]); + data.m_ch_ua = ua?.join?.(','); + data.m_ch_full_ver = fullVer?.join?.(','); + }, + 'mobile', isMobile => data.m_ch_mobile = `?${isMobile}`, + 'model', model => data.m_ch_model = model, + 'platform', platform => { + data.m_ch_platform = platform?.brand; + data.m_ch_platform_ver = platform?.version?.join?.('.'); + } + ]) + } } else { if (Object.keys(impExt).length) { mergeDeep(data.imp[0].ext, impExt); diff --git a/modules/schain.js b/modules/schain.js index 2991bb5b3d5..726679b133f 100644 --- a/modules/schain.js +++ b/modules/schain.js @@ -1,16 +1,17 @@ -import { config } from '../src/config.js'; +import {config} from '../src/config.js'; import adapterManager from '../src/adapterManager.js'; import { - isNumber, - isStr, + _each, + deepAccess, + deepClone, + deepSetValue, isArray, + isInteger, + isNumber, isPlainObject, - hasOwn, + isStr, logError, - isInteger, - _each, - logWarn, - deepAccess, deepSetValue, deepClone + logWarn } from '../src/utils.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; @@ -63,7 +64,7 @@ export function isSchainObjectValid(schainObject, returnOnError) { } // ext: Object [optional] - if (hasOwn(schainObject, 'ext')) { + if (schainObject.hasOwnProperty('ext')) { if (!isPlainObject(schainObject.ext)) { appendFailMsg(`schain.config.ext` + shouldBeAnObject); } @@ -92,28 +93,28 @@ export function isSchainObjectValid(schainObject, returnOnError) { } // rid: String [Optional] - if (hasOwn(node, 'rid')) { + if (node.hasOwnProperty('rid')) { if (!isStr(node.rid)) { appendFailMsg(`schain.config.nodes[${index}].rid` + shouldBeAString); } } // name: String [Optional] - if (hasOwn(node, 'name')) { + if (node.hasOwnProperty('name')) { if (!isStr(node.name)) { appendFailMsg(`schain.config.nodes[${index}].name` + shouldBeAString); } } // domain: String [Optional] - if (hasOwn(node, 'domain')) { + if (node.hasOwnProperty('domain')) { if (!isStr(node.domain)) { appendFailMsg(`schain.config.nodes[${index}].domain` + shouldBeAString); } } // ext: Object [Optional] - if (hasOwn(node, 'ext')) { + if (node.hasOwnProperty('ext')) { if (!isPlainObject(node.ext)) { appendFailMsg(`schain.config.nodes[${index}].ext` + shouldBeAnObject); } diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index ef56e10870b..9046d6a633d 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -13,7 +13,7 @@ import {VENDORLESS_GVLID} from '../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; import {domainOverrideToRootDomain} from '../libraries/domainOverrideToRootDomain/index.js'; -export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'pubCommonId'}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'sharedId'}); const COOKIE = 'cookie'; const LOCAL_STORAGE = 'html5'; const OPTOUT_NAME = '_pubcid_optout'; @@ -174,6 +174,12 @@ export const sharedIdSystemSubmodule = { }, domainOverride: domainOverrideToRootDomain(storage, 'sharedId'), + eids: { + 'pubcid': { + source: 'pubcid.org', + atype: 1 + }, + } }; submodule('userId', sharedIdSystemSubmodule); diff --git a/modules/sharethroughAnalyticsAdapter.js b/modules/sharethroughAnalyticsAdapter.js index 6502c7e3a53..dc621e8da92 100644 --- a/modules/sharethroughAnalyticsAdapter.js +++ b/modules/sharethroughAnalyticsAdapter.js @@ -1,6 +1,6 @@ -import { tryAppendQueryString } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index d3f4b456aeb..3684e793dcb 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,7 +1,7 @@ -import { deepAccess, generateUUID, inIframe } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { deepAccess, generateUUID, inIframe } from '../src/utils.js'; const VERSION = '4.3.0'; const BIDDER_CODE = 'sharethrough'; @@ -18,14 +18,14 @@ export const sharethroughAdapterSpec = { code: BIDDER_CODE, supportedMediaTypes: [VIDEO, BANNER], gvlid: 80, - isBidRequestValid: bid => !!bid.params.pkey && bid.bidder === BIDDER_CODE, + isBidRequestValid: (bid) => !!bid.params.pkey && bid.bidder === BIDDER_CODE, buildRequests: (bidRequests, bidderRequest) => { const timeout = bidderRequest.timeout; const firstPartyData = bidderRequest.ortb2 || {}; const nonHttp = sharethroughInternal.getProtocol().indexOf('http') < 0; - const secure = nonHttp || (sharethroughInternal.getProtocol().indexOf('https') > -1); + const secure = nonHttp || sharethroughInternal.getProtocol().indexOf('https') > -1; const req = { id: generateUUID(), @@ -79,64 +79,79 @@ export const sharethroughAdapterSpec = { req.regs.ext.us_privacy = bidderRequest.uspConsent; } - const imps = bidRequests.map(bidReq => { - const impression = { ext: {} }; - - // mergeDeep(impression, bidReq.ortb2Imp); // leaving this out for now as we may want to leave stuff out on purpose - const tid = deepAccess(bidReq, 'ortb2Imp.ext.tid'); - if (tid) impression.ext.tid = tid; - const gpid = deepAccess(bidReq, 'ortb2Imp.ext.gpid', deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot')); - if (gpid) impression.ext.gpid = gpid; - - const videoRequest = deepAccess(bidReq, 'mediaTypes.video'); + if (bidderRequest?.gppConsent?.gppString) { + req.regs.gpp = bidderRequest.gppConsent.gppString; + req.regs.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest?.ortb2?.regs?.gpp) { + req.regs.ext.gpp = bidderRequest.ortb2.regs.gpp; + req.regs.ext.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } - if (videoRequest) { - // default playerSize, only change this if we know width and height are properly defined in the request - let [w, h] = [640, 360]; - if (videoRequest.playerSize && videoRequest.playerSize[0] && videoRequest.playerSize[0][0] && videoRequest.playerSize[0][1]) { - [w, h] = videoRequest.playerSize[0]; + const imps = bidRequests + .map((bidReq) => { + const impression = { ext: {} }; + + // mergeDeep(impression, bidReq.ortb2Imp); // leaving this out for now as we may want to leave stuff out on purpose + const tid = deepAccess(bidReq, 'ortb2Imp.ext.tid'); + if (tid) impression.ext.tid = tid; + const gpid = deepAccess(bidReq, 'ortb2Imp.ext.gpid', deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot')); + if (gpid) impression.ext.gpid = gpid; + + const videoRequest = deepAccess(bidReq, 'mediaTypes.video'); + + if (videoRequest) { + // default playerSize, only change this if we know width and height are properly defined in the request + let [w, h] = [640, 360]; + if ( + videoRequest.playerSize && + videoRequest.playerSize[0] && + videoRequest.playerSize[0][0] && + videoRequest.playerSize[0][1] + ) { + [w, h] = videoRequest.playerSize[0]; + } + + impression.video = { + pos: nullish(videoRequest.pos, 0), + topframe: inIframe() ? 0 : 1, + skip: nullish(videoRequest.skip, 0), + linearity: nullish(videoRequest.linearity, 1), + minduration: nullish(videoRequest.minduration, 5), + maxduration: nullish(videoRequest.maxduration, 60), + playbackmethod: videoRequest.playbackmethod || [2], + api: getVideoApi(videoRequest), + mimes: videoRequest.mimes || ['video/mp4'], + protocols: getVideoProtocols(videoRequest), + w, + h, + startdelay: nullish(videoRequest.startdelay, 0), + skipmin: nullish(videoRequest.skipmin, 0), + skipafter: nullish(videoRequest.skipafter, 0), + placement: videoRequest.context === 'instream' ? 1 : +deepAccess(videoRequest, 'placement', 4), + }; + + if (videoRequest.delivery) impression.video.delivery = videoRequest.delivery; + if (videoRequest.companiontype) impression.video.companiontype = videoRequest.companiontype; + if (videoRequest.companionad) impression.video.companionad = videoRequest.companionad; + } else { + impression.banner = { + pos: deepAccess(bidReq, 'mediaTypes.banner.pos', 0), + topframe: inIframe() ? 0 : 1, + format: bidReq.sizes.map((size) => ({ w: +size[0], h: +size[1] })), + }; } - impression.video = { - pos: nullish(videoRequest.pos, 0), - topframe: inIframe() ? 0 : 1, - skip: nullish(videoRequest.skip, 0), - linearity: nullish(videoRequest.linearity, 1), - minduration: nullish(videoRequest.minduration, 5), - maxduration: nullish(videoRequest.maxduration, 60), - playbackmethod: videoRequest.playbackmethod || [2], - api: getVideoApi(videoRequest), - mimes: videoRequest.mimes || ['video/mp4'], - protocols: getVideoProtocols(videoRequest), - w, - h, - startdelay: nullish(videoRequest.startdelay, 0), - skipmin: nullish(videoRequest.skipmin, 0), - skipafter: nullish(videoRequest.skipafter, 0), - placement: videoRequest.context === 'instream' ? 1 : +deepAccess(videoRequest, 'placement', 4), - }; - - if (videoRequest.delivery) impression.video.delivery = videoRequest.delivery; - if (videoRequest.companiontype) impression.video.companiontype = videoRequest.companiontype; - if (videoRequest.companionad) impression.video.companionad = videoRequest.companionad; - } else { - impression.banner = { - pos: deepAccess(bidReq, 'mediaTypes.banner.pos', 0), - topframe: inIframe() ? 0 : 1, - format: bidReq.sizes.map(size => ({ w: +size[0], h: +size[1] })), + return { + id: bidReq.bidId, + tagid: String(bidReq.params.pkey), + secure: secure ? 1 : 0, + bidfloor: getBidRequestFloor(bidReq), + ...impression, }; - } - - return { - id: bidReq.bidId, - tagid: String(bidReq.params.pkey), - secure: secure ? 1 : 0, - bidfloor: getBidRequestFloor(bidReq), - ...impression, - }; - }).filter(imp => !!imp); + }) + .filter((imp) => !!imp); - return imps.map(impression => { + return imps.map((impression) => { return { method: 'POST', url: STR_ENDPOINT, @@ -149,11 +164,17 @@ export const sharethroughAdapterSpec = { }, interpretResponse: ({ body }, req) => { - if (!body || !body.seatbid || body.seatbid.length === 0 || !body.seatbid[0].bid || body.seatbid[0].bid.length === 0) { + if ( + !body || + !body.seatbid || + body.seatbid.length === 0 || + !body.seatbid[0].bid || + body.seatbid[0].bid.length === 0 + ) { return []; } - return body.seatbid[0].bid.map(bid => { + return body.seatbid[0].bid.map((bid) => { // Spec: https://docs.prebid.org/dev-docs/bidder-adaptor.html#interpreting-the-response const response = { requestId: bid.impid, @@ -196,24 +217,22 @@ export const sharethroughAdapterSpec = { }, getUserSyncs: (syncOptions, serverResponses) => { - const shouldCookieSync = syncOptions.pixelEnabled && deepAccess(serverResponses, '0.body.cookieSyncUrls') !== undefined; + const shouldCookieSync = + syncOptions.pixelEnabled && deepAccess(serverResponses, '0.body.cookieSyncUrls') !== undefined; return shouldCookieSync - ? serverResponses[0].body.cookieSyncUrls.map(url => ({ type: 'image', url: url })) + ? serverResponses[0].body.cookieSyncUrls.map((url) => ({ type: 'image', url: url })) : []; }, // Empty implementation for prebid core to be able to find it - onTimeout: (data) => { - }, + onTimeout: (data) => {}, // Empty implementation for prebid core to be able to find it - onBidWon: (bid) => { - }, + onBidWon: (bid) => {}, // Empty implementation for prebid core to be able to find it - onSetTargeting: (bid) => { - }, + onSetTargeting: (bid) => {}, }; function getVideoApi({ api }) { @@ -240,7 +259,7 @@ function getBidRequestFloor(bid) { const floorInfo = bid.getFloor({ currency: 'USD', mediaType: bid.mediaTypes && bid.mediaTypes.video ? 'video' : 'banner', - size: bid.sizes.map(size => ({ w: size[0], h: size[1] })), + size: bid.sizes.map((size) => ({ w: size[0], h: size[1] })), }); if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { floor = parseFloat(floorInfo.floor); diff --git a/modules/shinezBidAdapter.js b/modules/shinezBidAdapter.js index 96b6d281fdc..47fca317de2 100644 --- a/modules/shinezBidAdapter.js +++ b/modules/shinezBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; diff --git a/modules/showheroes-bsBidAdapter.js b/modules/showheroes-bsBidAdapter.js index a241cb71a5d..bd2706a21d5 100644 --- a/modules/showheroes-bsBidAdapter.js +++ b/modules/showheroes-bsBidAdapter.js @@ -1,10 +1,9 @@ import { deepAccess, - getBidIdParameter, getWindowTop, triggerPixel, logInfo, - logError + logError, getBidIdParameter } from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; @@ -29,8 +28,11 @@ function getEnvURLs(isStage) { } } +const GVLID = 111; + export const spec = { code: BIDDER_CODE, + gvlid: GVLID, aliases: ['showheroesBs'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function(bid) { diff --git a/modules/silverpushBidAdapter.js b/modules/silverpushBidAdapter.js index df81e144380..5403f3bd88c 100644 --- a/modules/silverpushBidAdapter.js +++ b/modules/silverpushBidAdapter.js @@ -12,7 +12,7 @@ const bidderConfig = 'sp_pb_ortb'; const bidderVersion = '1.0.0'; const DEFAULT_CURRENCY = 'USD'; -export const REQUEST_URL = 'https://apac.chocolateplatform.com/bidder/?identifier=prebidchoc'; +export const REQUEST_URL = 'https://prebid.chocolateplatform.co/bidder/?identifier=prebidchoc'; export const SP_OUTSTREAM_PLAYER_URL = 'https://xaido.sgp1.cdn.digitaloceanspaces.com/prebid/spoutstream.min.js'; const VIDEO_ORTB_PARAMS = [ @@ -110,7 +110,7 @@ export const CONVERTER = ortbConverter({ bidResponse.meta.paf.content_id = utils.deepAccess(bid, 'ext.paf.content_id'); } - bidResponse = buildVideoVastResponse(bidResponse) + bidResponse = buildVideoVastResponse(bidResponse); bidResponse = buildVideoOutstreamResponse(bidResponse, context) return bidResponse; diff --git a/modules/slimcutBidAdapter.js b/modules/slimcutBidAdapter.js index 447e314958f..c3f06556652 100644 --- a/modules/slimcutBidAdapter.js +++ b/modules/slimcutBidAdapter.js @@ -1,4 +1,4 @@ -import { getValue, parseSizesInput, getBidIdParameter } from '../src/utils.js'; +import {getBidIdParameter, getValue, parseSizesInput} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index 1b50e033074..b735953d099 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -1,22 +1,12 @@ -import { - chunk, - deepAccess, - deepSetValue, - fill, - getAdUnitSizes, - getDNT, - getMaxValueFromArray, - getMinValueFromArray, - isEmpty, - isNumber, - logError, - logInfo -} from '../src/utils.js'; +import {deepAccess, deepSetValue, getDNT, isEmpty, isNumber, logError, logInfo} from '../src/utils.js'; import {find} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import CONSTANTS from '../src/constants.json'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {fill} from '../libraries/appnexusUtils/anUtils.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const { NATIVE_IMAGE_TYPES } = CONSTANTS; const BIDDER_CODE = 'smaato'; @@ -466,7 +456,7 @@ function createAdPodImp(bidRequest, videoMediaType) { }); } else { // all maxdurations should be the same - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); imps.map((imp, index) => { const sequence = index + 1; imp.video.maxduration = maxDuration @@ -481,7 +471,7 @@ function createAdPodImp(bidRequest, videoMediaType) { function getAdPodNumberOfPlacements(videoMediaType) { const {adPodDurationSec, durationRangeSec, requireExactDuration} = videoMediaType - const minAllowedDuration = getMinValueFromArray(durationRangeSec) + const minAllowedDuration = Math.min(...durationRangeSec) const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration) return requireExactDuration diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js index d91b62729bc..45cc45192ef 100644 --- a/modules/smartxBidAdapter.js +++ b/modules/smartxBidAdapter.js @@ -1,4 +1,17 @@ -import { logError, deepAccess, isArray, getBidIdParameter, getDNT, generateUUID, isEmpty, _each, logMessage, logWarn, isFn, isPlainObject } from '../src/utils.js'; +import { + logError, + deepAccess, + isArray, + getDNT, + generateUUID, + isEmpty, + _each, + logMessage, + logWarn, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { Renderer } from '../src/Renderer.js'; diff --git a/modules/smartxBidAdapter.md b/modules/smartxBidAdapter.md index 853f06d6baf..50f78660458 100644 --- a/modules/smartxBidAdapter.md +++ b/modules/smartxBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: smartclip Bidder Adapter Module Type: Bidder Adapter -Maintainer: adtech@smartclip.tv +Maintainer: bidding@smartclip.tv ``` # Description @@ -170,4 +170,4 @@ This adapter requires setup and approval from the smartclip team. } }], }]; -``` \ No newline at end of file +``` diff --git a/modules/smartyadsBidAdapter.js b/modules/smartyadsBidAdapter.js index 89749aed433..2409bebbc59 100644 --- a/modules/smartyadsBidAdapter.js +++ b/modules/smartyadsBidAdapter.js @@ -3,9 +3,16 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { ajax } from '../src/ajax.js'; const BIDDER_CODE = 'smartyads'; -const AD_URL = 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js'; +const GVLID = 534; +const adUrls = { + US_EAST: 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + EU: 'https://n2.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + SGP: 'https://n6.smartyads.com/?c=o&m=prebid&secret_key=prebid_js' +} + const URL_SYNC = 'https://as.ck-ie.com/prebidjs?p=7c47322e527cf8bdeb7facc1bb03387a'; function isBidResponseValid(bid) { @@ -25,8 +32,28 @@ function isBidResponseValid(bid) { } } +function getAdUrlByRegion(bid) { + let adUrl; + + if (bid.params.region && adUrls[bid.params.region]) { + adUrl = adUrls[bid.params.region]; + } else { + try { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const region = timezone.split('/')[0]; + if (region === 'Europe') adUrl = adUrls['EU']; + else adUrl = adUrls['US_EAST']; + } catch (err) { + adUrl = adUrls['US_EAST']; + } + } + + return adUrl; +} + export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: (bid) => { @@ -66,11 +93,17 @@ export const spec = { if (bidderRequest.gdprConsent) { request.gdpr = bidderRequest.gdprConsent } + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent; + } } const len = validBidRequests.length; + let adUrl; + for (let i = 0; i < len; i++) { let bid = validBidRequests[i]; + if (i === 0) adUrl = getAdUrlByRegion(bid); let traff = bid.params.traffic || BANNER placements.push({ placementId: bid.params.sourceid, @@ -83,11 +116,12 @@ export const spec = { placements.schain = bid.schain; } } + return { method: 'POST', - url: AD_URL, + url: adUrl, data: request - }; + } }, interpretResponse: (serverResponse) => { @@ -102,24 +136,46 @@ export const spec = { return response; }, - getUserSyncs: (syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '') => { + getUserSyncs: (syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = '') => { let syncs = []; let { gdprApplies, consentString = '' } = gdprConsent; if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&type=iframe&us_privacy=${uspConsent}` + url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&type=iframe&us_privacy=${uspConsent}&gpp=${gppConsent}` }); } else { syncs.push({ type: 'image', - url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&type=image&us_privacy=${uspConsent}` + url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&type=image&us_privacy=${uspConsent}&gpp=${gppConsent}` }); } return syncs - } + }, + + onBidWon: function(bid) { + if (bid.winUrl) { + ajax(bid.winUrl, () => {}, JSON.stringify(bid)); + } else { + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&winTest=1', () => {}, JSON.stringify(bid)); + } + } + }, + + onTimeout: function(bid) { + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidTimeout=1', () => {}, JSON.stringify(bid)); + } + }, + + onBidderError: function(bid) { + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidderError=1', () => {}, JSON.stringify(bid)); + } + }, }; diff --git a/modules/smartyadsBidAdapter.md b/modules/smartyadsBidAdapter.md index e0d6023a794..443d5ab5978 100644 --- a/modules/smartyadsBidAdapter.md +++ b/modules/smartyadsBidAdapter.md @@ -14,10 +14,11 @@ Module that connects to SmartyAds' demand sources | Name | Scope | Description | Example | | :------------ | :------- | :------------------------ | :------------------- | -| `sourceid` | required (for prebid.js) | placement ID | "0" | -| `host` | required (for prebid-server) | const value, set to "prebid" | "prebid" | -| `accountid` | required (for prebid-server) | partner ID | "1901" | +| `sourceid` | required (for prebid.js) | Placement ID | "0" | +| `host` | required (for prebid-server) | Const value, set to "prebid" | "prebid" | +| `accountid` | required (for prebid-server) | Partner ID | "1901" | | `traffic` | optional (for prebid.js) | Configures the mediaType that should be used. Values can be banner, native or video | "banner" | +| `region` | optional (for prebid.js) | Prefix of the region to which prebid must send requests. Possible values: "US_EAST", "EU" | "US_EAST" | # Test Parameters ``` @@ -35,7 +36,9 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'native' + traffic: 'native', + region: 'US_EAST' + } } ] @@ -55,7 +58,8 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'banner' + traffic: 'banner', + region: 'US_EAST' } } ] @@ -76,7 +80,9 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'video' + traffic: 'video', + region: 'US_EAST' + } } ] diff --git a/modules/smilewantedBidAdapter.js b/modules/smilewantedBidAdapter.js index b7a25ba58df..2fbfcaa79af 100644 --- a/modules/smilewantedBidAdapter.js +++ b/modules/smilewantedBidAdapter.js @@ -4,9 +4,12 @@ import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +const GVL_ID = 639; + export const spec = { code: 'smilewanted', aliases: ['smile', 'sw'], + gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. diff --git a/modules/snigelBidAdapter.js b/modules/snigelBidAdapter.js index f41fb98d436..6d32d8f97a2 100644 --- a/modules/snigelBidAdapter.js +++ b/modules/snigelBidAdapter.js @@ -1,7 +1,7 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import {deepAccess, isArray, isFn, isPlainObject} from '../src/utils.js'; +import {deepAccess, isArray, isFn, isPlainObject, inIframe, getDNT} from '../src/utils.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import {getGlobal} from '../src/prebidGlobal.js'; @@ -13,6 +13,7 @@ const DEFAULT_CURRENCIES = ['USD']; const FLOOR_MATCH_ALL_SIZES = '*'; const getConfig = config.getConfig; +const refreshes = {}; export const spec = { code: BIDDER_CODE, @@ -29,12 +30,15 @@ export const spec = { method: 'POST', url: getEndpoint(), data: JSON.stringify({ - id: bidderRequest.bidderRequestId, + id: bidderRequest.auctionId, + accountId: deepAccess(bidRequests, '0.params.accountId'), + site: deepAccess(bidRequests, '0.params.site'), cur: getCurrencies(), test: getTestFlag(), - devw: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, - devh: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, version: getGlobal().version, + gpp: deepAccess(bidderRequest, 'gppConsent.gppString') || deepAccess(bidderRequest, 'ortb2.regs.gpp'), + gpp_sid: + deepAccess(bidderRequest, 'gppConsent.applicableSections') || deepAccess(bidderRequest, 'ortb2.regs.gpp_sid'), gdprApplies: gdprApplies, gdprConsentString: gdprApplies === true ? deepAccess(bidderRequest, 'gdprConsent.consentString') : undefined, gdprConsentProv: gdprApplies === true ? deepAccess(bidderRequest, 'gdprConsent.addtlConsent') : undefined, @@ -43,12 +47,24 @@ export const spec = { eids: deepAccess(bidRequests, '0.userIdAsEids'), schain: deepAccess(bidRequests, '0.schain'), page: getPage(bidderRequest), + topframe: inIframe() === true ? 0 : 1, + device: { + w: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, + h: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, + dnt: getDNT() ? 1 : 0, + language: getLanguage(), + }, placements: bidRequests.map((r) => { return { - uuid: r.bidId, + id: r.adUnitCode, + tid: r.transactionId, + gpid: deepAccess(r, 'ortb2Imp.ext.gpid'), + pbadslot: deepAccess(r, 'ortb2Imp.ext.data.pbadslot') || deepAccess(r, 'ortb2Imp.ext.gpid'), name: r.params.placement, sizes: r.sizes, floor: getPriceFloor(r, BANNER, FLOOR_MATCH_ALL_SIZES), + refresh: getRefreshInformation(r.adUnitCode), + params: r.params.additionalParams, }; }), }), @@ -56,14 +72,14 @@ export const spec = { }; }, - interpretResponse: function (serverResponse) { + interpretResponse: function (serverResponse, bidRequest) { if (!serverResponse.body || !serverResponse.body.bids) { return []; } return serverResponse.body.bids.map((bid) => { return { - requestId: bid.uuid, + requestId: mapIdToRequestId(bid.id, bidRequest), cpm: bid.price, creativeId: bid.crid, currency: serverResponse.body.cur, @@ -77,9 +93,9 @@ export const spec = { }); }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncUrl = getSyncUrl(responses || []); - if (syncUrl && syncOptions.iframeEnabled && hasSyncConsent(gdprConsent, uspConsent)) { + if (syncUrl && syncOptions.iframeEnabled && hasSyncConsent(gdprConsent, uspConsent, gppConsent)) { return [{type: 'iframe', url: getSyncEndpoint(syncUrl, gdprConsent)}]; } }, @@ -89,7 +105,7 @@ registerBidder(spec); function getPage(bidderRequest) { return ( - getConfig(`${BIDDER_CODE}.page`) || deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || window.location.href + getConfig(`${BIDDER_CODE}.page`) || deepAccess(bidderRequest, 'refererInfo.page') || window.location.href ); } @@ -101,6 +117,14 @@ function getTestFlag() { return getConfig(`${BIDDER_CODE}.test`) === true; } +function getLanguage() { + return navigator && navigator.language + ? navigator.language.indexOf('-') != -1 + ? navigator.language.split('-')[0] + : navigator.language + : undefined; +} + function getCurrencies() { const currencyOverrides = getConfig(`${BIDDER_CODE}.cur`); if (currencyOverrides !== undefined && (!isArray(currencyOverrides) || currencyOverrides.length === 0)) { @@ -130,14 +154,43 @@ function getPriceFloor(bidRequest, mediaType, size) { } } -function hasSyncConsent(gdprConsent, uspConsent) { - if (gdprConsent?.gdprApplies && !hasPurpose1Consent(gdprConsent)) { - return false; - } else if (uspConsent && uspConsent[1] === 'Y' && uspConsent[2] === 'Y') { - return false; - } else { - return true; +function getRefreshInformation(adUnitCode) { + const refresh = refreshes[adUnitCode]; + if (!refresh) { + refreshes[adUnitCode] = { + count: 0, + previousTime: new Date(), + }; + return undefined; } + + const currentTime = new Date(); + const timeDifferenceSeconds = Math.floor((currentTime - refresh.previousTime) / 1000); + refresh.count += 1; + refresh.previousTime = currentTime; + return { + count: refresh.count, + time: timeDifferenceSeconds, + }; +} + +function mapIdToRequestId(id, bidRequest) { + return bidRequest.bidderRequest.bids.filter((bid) => bid.adUnitCode === id)[0].bidId; +} + +function hasUspConsent(uspConsent) { + return typeof uspConsent !== 'string' || !(uspConsent[0] === '1' && uspConsent[2] === 'Y'); +} + +function hasGppConsent(gppConsent) { + return ( + !(gppConsent && Array.isArray(gppConsent.applicableSections)) || + gppConsent.applicableSections.every((section) => typeof section === 'number' && section <= 5) + ); +} + +function hasSyncConsent(gdprConsent, uspConsent, gppConsent) { + return hasPurpose1Consent(gdprConsent) && hasUspConsent(uspConsent) && hasGppConsent(gppConsent); } function getSyncUrl(responses) { diff --git a/modules/snigelBidAdapter.md b/modules/snigelBidAdapter.md index a83e133144f..f9bb1951d21 100644 --- a/modules/snigelBidAdapter.md +++ b/modules/snigelBidAdapter.md @@ -15,9 +15,13 @@ Please reach out to us [through our contact form](https://snigel.com/get-in-touc # Parameters -| Name | Required | Description | Example | -| :--- | :-------- | :---------- | :------ | -| placement | Yes | Placement identifier | top_leaderboard | +| Name | Required | Description | +| :-------- | :------- | :------------------- | +| accountId | Yes | Account identifier | +| site | Yes | Site identifier | +| placement | Yes | Placement identifier | + +Snigel will provide all of these parameters to you. # Test @@ -37,6 +41,8 @@ var adUnits = [ { bidder: "snigel", params: { + accountId: "1000", + site: "test.com", placement: "prebid_test_placement", }, }, diff --git a/modules/sonobiAnalyticsAdapter.js b/modules/sonobiAnalyticsAdapter.js index 0057944b201..04a855b5be6 100644 --- a/modules/sonobiAnalyticsAdapter.js +++ b/modules/sonobiAnalyticsAdapter.js @@ -6,7 +6,7 @@ import {ajaxBuilder} from '../src/ajax.js'; let ajax = ajaxBuilder(0); -const DEFAULT_EVENT_URL = 'apex.go.sonobi.com/keymaker'; +export const DEFAULT_EVENT_URL = 'apex.go.sonobi.com/keymaker'; const analyticsType = 'endpoint'; const QUEUE_TIMEOUT_DEFAULT = 200; const { diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index 704275cc1bf..a2d1f385623 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -1,11 +1,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, getGptSlotInfoForAdUnitCode, isFn, isPlainObject } from '../src/utils.js'; +import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, isFn, isPlainObject } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { userSync } from '../src/userSync.js'; import { bidderSettings } from '../src/bidderSettings.js'; import { getAllOrtbKeywords } from '../libraries/keywords/keywords.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'sonobi'; const STR_ENDPOINT = 'https://apex.go.sonobi.com/trinity.json'; const PAGEVIEW_ID = generateUUID(); @@ -60,23 +61,15 @@ export const spec = { */ buildRequests: (validBidRequests, bidderRequest) => { const bids = validBidRequests.map(bid => { - let mediaType; - - if (deepAccess(bid, 'mediaTypes.video')) { - mediaType = 'video'; - } else if (deepAccess(bid, 'mediaTypes.banner')) { - mediaType = 'display'; - } - let slotIdentifier = _validateSlot(bid); if (/^[\/]?[\d]+[[\/].+[\/]?]?$/.test(slotIdentifier)) { slotIdentifier = slotIdentifier.charAt(0) === '/' ? slotIdentifier : '/' + slotIdentifier; return { - [`${slotIdentifier}|${bid.bidId}`]: `${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(mediaType)}` + [`${slotIdentifier}|${bid.bidId}`]: `${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(bid)}` } } else if (/^[0-9a-fA-F]{20}$/.test(slotIdentifier) && slotIdentifier.length === 20) { return { - [bid.bidId]: `${slotIdentifier}|${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(mediaType)}` + [bid.bidId]: `${slotIdentifier}|${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(bid)}` } } else { logError(`The ad unit code or Sonobi Placement id for slot ${bid.bidId} is invalid`); @@ -336,10 +329,28 @@ function _validateGPID(bid) { return '' } -function _validateMediaType(mediaType) { +function _validateMediaType(bidRequest) { + let mediaType; + if (deepAccess(bidRequest, 'mediaTypes.video')) { + mediaType = 'video'; + } else if (deepAccess(bidRequest, 'mediaTypes.banner')) { + mediaType = 'display'; + } + let mediaTypeValidation = ''; if (mediaType === 'video') { mediaTypeValidation = 'c=v,'; + if (deepAccess(bidRequest, 'mediaTypes.video.playbackmethod')) { + mediaTypeValidation = `${mediaTypeValidation}pm=${deepAccess(bidRequest, 'mediaTypes.video.playbackmethod').join(':')},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.placement')) { + let placement = deepAccess(bidRequest, 'mediaTypes.video.placement'); + mediaTypeValidation = `${mediaTypeValidation}p=${placement},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.plcmt')) { + let plcmt = deepAccess(bidRequest, 'mediaTypes.video.plcmt'); + mediaTypeValidation = `${mediaTypeValidation}pl=${plcmt},`; + } } else if (mediaType === 'display') { mediaTypeValidation = 'c=d,'; } diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index f21c4d1752a..f72c482735a 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,13 +1,12 @@ import { _each, - getBidIdParameter, isArray, getUniqueIdentifierStr, deepSetValue, logError, deepAccess, isInteger, - logWarn + logWarn, getBidIdParameter } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' import { @@ -45,7 +44,6 @@ const ORTB_VIDEO_PARAMS = { const REQUIRED_VIDEO_PARAMS = { context: (value) => value !== ADPOD, mimes: ORTB_VIDEO_PARAMS.mimes, - minduration: ORTB_VIDEO_PARAMS.minduration, maxduration: ORTB_VIDEO_PARAMS.maxduration, protocols: ORTB_VIDEO_PARAMS.protocols } @@ -169,6 +167,11 @@ export const spec = { }; } + const tid = deepAccess(bidderRequest, 'ortb2.source.tid') + if (tid) { + deepSetValue(sovrnBidReq, 'source.tid', tid) + } + if (bidderRequest.gdprConsent) { deepSetValue(sovrnBidReq, 'regs.ext.gdpr', +bidderRequest.gdprConsent.gdprApplies); deepSetValue(sovrnBidReq, 'user.ext.consent', bidderRequest.gdprConsent.consentString) @@ -243,7 +246,7 @@ export const spec = { } }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { try { const tracks = [] if (serverResponses && serverResponses.length !== 0) { @@ -257,6 +260,10 @@ export const spec = { if (uspConsent) { params.push(['us_privacy', uspConsent]); } + if (gppConsent) { + params.push(['gpp', gppConsent.gppString]); + params.push(['gpp_sid', gppConsent.applicableSections]) + } if (iidArr[0]) { params.push(['informer', iidArr[0]]); diff --git a/modules/sparteoBidAdapter.js b/modules/sparteoBidAdapter.js new file mode 100644 index 00000000000..7d4f87c24f3 --- /dev/null +++ b/modules/sparteoBidAdapter.js @@ -0,0 +1,126 @@ +import { deepAccess, deepSetValue, logError, parseSizesInput, triggerPixel } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +const BIDDER_CODE = 'sparteo'; +const GVLID = 1028; +const TTL = 60; +const HTTP_METHOD = 'POST'; +const REQUEST_URL = 'https://bid.sparteo.com/auction'; + +const converter = ortbConverter({ + context: { + // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: TTL // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + + if (bidderRequest.bids[0].params.networkId) { + deepSetValue(request, 'site.publisher.ext.params.networkId', bidderRequest.bids[0].params.networkId); + } + + if (bidderRequest.bids[0].params.publisherId) { + deepSetValue(request, 'site.publisher.ext.params.publisherId', bidderRequest.bids[0].params.publisherId); + } + + return request; + }, + bidResponse(buildBidResponse, bid, context) { + context.mediaType = deepAccess(bid, 'ext.prebid.type'); + + return buildBidResponse(bid, context) + } +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + let bannerParams = deepAccess(bid, 'mediaTypes.banner'); + let videoParams = deepAccess(bid, 'mediaTypes.video'); + + if (!bid.params) { + logError('The bid params are missing'); + return false; + } + + if (!bid.params.networkId && !bid.params.publisherId) { + logError('The networkId or publisherId is required'); + return false; + } + + if (!bannerParams && !videoParams) { + logError('The placement must be of banner or video type'); + return false; + } + + /** + * BANNER checks + */ + + if (bannerParams) { + let sizes = bannerParams.sizes; + + if (!sizes || parseSizesInput(sizes).length == 0) { + logError('mediaTypes.banner.sizes must be set for banner placement at the right format.'); + return false; + } + } + + /** + * VIDEO checks + */ + + if (videoParams) { + if (parseSizesInput(videoParams.playerSize).length == 0) { + logError('mediaTypes.video.playerSize must be set for video placement at the right format.'); + return false; + } + } + + return true; + }, + + buildRequests: function(bidRequests, bidderRequest) { + const payload = converter.toORTB({bidRequests, bidderRequest}) + + return { + method: HTTP_METHOD, + url: bidRequests[0].params.endpoint ? bidRequests[0].params.endpoint : REQUEST_URL, + data: payload + }; + }, + + interpretResponse: function(serverResponse, requests) { + const bids = converter.fromORTB({response: serverResponse.body, request: requests.data}).bids; + + return bids; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) {}, + + onTimeout: function(timeoutData) {}, + + onBidWon: function(bid) { + if (bid && bid.nurl && bid.nurl.length > 0) { + bid.nurl.forEach(function(winUrl) { + triggerPixel(winUrl, null); + }); + } + }, + + onSetTargeting: function(bid) {} +}; + +registerBidder(spec); diff --git a/modules/sparteoBidAdapter.md b/modules/sparteoBidAdapter.md new file mode 100644 index 00000000000..774d9211d9d --- /dev/null +++ b/modules/sparteoBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: Sparteo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@sparteo.com +``` + +# Description + +Module that connects to Sparteo's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [ + [1, 1] + ] + } + }, + bids: [ + { + bidder: 'sparteo', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index 874207adcf8..017544cc596 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -1,4 +1,20 @@ -import { logError, deepAccess, isArray, getBidIdParameter, getDNT, deepSetValue, isEmpty, _each, logMessage, logWarn, isBoolean, isNumber, isPlainObject, isFn, setScriptAttributes } from '../src/utils.js'; +import { + logError, + deepAccess, + isArray, + getDNT, + deepSetValue, + isEmpty, + _each, + logMessage, + logWarn, + isBoolean, + isNumber, + isPlainObject, + isFn, + setScriptAttributes, + getBidIdParameter +} from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; diff --git a/modules/ssmasBidAdapter.js b/modules/ssmasBidAdapter.js new file mode 100644 index 00000000000..0b70a80e757 --- /dev/null +++ b/modules/ssmasBidAdapter.js @@ -0,0 +1,133 @@ +import { BANNER } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { triggerPixel, deepSetValue } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import {config} from '../src/config.js'; + +export const SSMAS_CODE = 'ssmas'; +const SSMAS_SERVER = 'ads.ssmas.com'; +export const SSMAS_ENDPOINT = `https://${SSMAS_SERVER}/ortb`; +const SYNC_URL = `https://sync.ssmas.com/user_sync`; +export const SSMAS_REQUEST_METHOD = 'POST'; +const GDPR_VENDOR_ID = 1183; + +export const ssmasOrtbConverter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + mediaType: BANNER, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'ext.placementId', bidRequest.params.placementId); + return imp; + }, +}); + +export const spec = { + code: SSMAS_CODE, + supportedMediaTypes: [BANNER], + gvlid: GDPR_VENDOR_ID, + + isBidRequestValid: (bid) => { + return !!bid.params.placementId && !!bid.bidId && bid.bidder === SSMAS_CODE; + }, + + buildRequests: (bidRequests, bidderRequest) => { + const data = ssmasOrtbConverter.toORTB({ bidRequests, bidderRequest }); + + const options = { + contentType: 'application/json', + withCredentials: false, + }; + + data.imp && data.imp.forEach(imp => { + if (imp.ext && imp.ext.placementId) { + imp.tagId = imp.ext.placementId; + } + }); + + data.regs = data.regs || {}; + data.regs.ext = data.regs.ext || {}; + + if (bidderRequest.gdprConsent) { + data.regs.ext.consent = bidderRequest.gdprConsent.consentString; + data.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + if (bidderRequest.uspConsent) { + data.regs.ext.consent = bidderRequest.uspConsent.consentString; + data.regs.ext.ccpa = 1; + } + if (config.getConfig('coppa') === true) { + data.regs.coppa = 1; + } + + return [ + { + method: SSMAS_REQUEST_METHOD, + url: SSMAS_ENDPOINT, + data, + options, + }, + ]; + }, + + interpretResponse: (serverResponse, bidRequest) => { + const bids = ssmasOrtbConverter.fromORTB({ + response: serverResponse.body, + request: bidRequest.data, + }).bids; + + return bids.filter((bid) => { + return bid.cpm > 0; + }); + }, + + onBidWon: (bid) => { + if (bid.burl) { + triggerPixel(bid.burl); + } + }, + + getUserSyncs: ( + syncOptions, + serverResponses, + gdprConsent, + uspConsent + ) => { + const syncs = []; + + let params = ['pbjs=1']; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params.push(`gdpr=${Boolean(gdprConsent.gdprApplies)}&gdpr_consent=${ + gdprConsent.consentString + }`); + } else { + params.push(`gdpr_consent=${gdprConsent.consentString}`); + } + } + + if (uspConsent && uspConsent.consentString) { + params.push(`ccpa_consent=${uspConsent.consentString}`); + } + + if (syncOptions.iframeEnabled && serverResponses.length > 0) { + syncs.push({ + type: 'iframe', + url: `${SYNC_URL}/iframe?${params.join('&')}` + }); + } + + // if (syncOptions.pixelEnabled && serverResponses.length > 0) { + // syncs.push({ + // type: 'image', + // url: `${SYNC_URL}/image?${params.join('&')}` + // }); + // } + return syncs; + }, +}; + +registerBidder(spec); diff --git a/modules/ssmasBidAdapter.md b/modules/ssmasBidAdapter.md new file mode 100644 index 00000000000..1b15764a54d --- /dev/null +++ b/modules/ssmasBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +Module Name: SSMas Bidder Adapter +Module Type: Bidder Adapter +Maintainer: hzchen.work@gmail.com + +# Description + +Module that connects to Sem Seo & Mas header bidding endpoint to fetch bids. +Supports Banner +Supported currencies: EUR + +Required parameters: +- placement id + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'ssmas', + params: { + placementId: "10336" + } + }] + } +]; +``` diff --git a/modules/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index 93ce26e7a48..93aa0973cc7 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -12,13 +12,14 @@ const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; const NOTIFY_URL = 'https://ssp.wp.pl/bidder/notify'; const GVLID = 676; const TMAX = 450; -const BIDDER_VERSION = '5.8'; +const BIDDER_VERSION = '5.92'; const DEFAULT_CURRENCY = 'PLN'; const W = window; const { navigator } = W; const oneCodeDetection = {}; const adUnitsCalled = {}; const adSizesCalled = {}; +const bidderRequestsMap = {}; const pageView = {}; var consentApiVersion; @@ -93,19 +94,17 @@ const getNotificationPayload = bidData => { const bids = isArray(bidData) ? bidData : [bidData]; if (bids.length > 0) { let result = { - requestId: undefined, siteId: [], slotId: [], tagid: [], } bids.forEach(bid => { - const { adUnitCode, auctionId, cpm, creativeId, meta, params: bidParams, requestId, timeout } = bid; + const { adUnitCode, cpm, creativeId, meta, mediaType, params: bidParams, bidderRequestId, requestId, timeout } = bid; const params = unpackParams(bidParams); // basic notification data const bidBasicData = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - requestId: auctionId || result.requestId, + requestId: bidderRequestId || bidderRequestsMap[requestId], timeout: timeout || result.timeout, pvid: pageView.id, } @@ -127,12 +126,14 @@ const getNotificationPayload = bidData => { if (cpm) { // non-empty bid data + const { advertiserDomains = [], networkName, pricepl } = meta; const bidNonEmptyData = { cpm, - cpmpl: meta && meta.pricepl, + cpmpl: pricepl, creativeId, - adomain: meta && meta.advertiserDomains && meta.advertiserDomains[0], - networkName: meta && meta.networkName, + adomain: advertiserDomains[0], + adtype: mediaType, + networkName, } result = { ...result, ...bidNonEmptyData } } @@ -198,6 +199,22 @@ const applyClientHints = ortbRequest => { ortbRequest.user = { ...ortbRequest.user, ...ch }; }; +const applyTopics = (validBidRequest, ortbRequest) => { + const userData = validBidRequest.ortb2?.user?.data || []; + const topicsData = userData.filter(dataObj => { + const segtax = dataObj.ext?.segtax; + return segtax >= 600 && segtax <= 609; + })[0]; + + // format topics obj for exchange + if (topicsData) { + topicsData.id = `${topicsData.ext.segtax}`; + topicsData.name = 'topics'; + delete (topicsData.ext); + ortbRequest.user.data.push(topicsData); + } +}; + const applyUserIds = (validBidRequest, ortbRequest) => { const eids = validBidRequest.userIdAsEids if (eids && eids.length) { @@ -447,10 +464,15 @@ var mapVideo = (slot, videoFromBid) => { }; const mapImpression = slot => { - const { adUnitCode, bidId, params = {}, ortb2Imp = {} } = slot; + const { adUnitCode, bidderRequestId, bidId, params = {}, ortb2Imp = {} } = slot; const { id, siteId, video } = params; const { ext = {} } = ortb2Imp; + /* + store bidId <-> bidderRequestId mapping for bidWon notification + */ + bidderRequestsMap[bidId] = bidderRequestId; + /* check max size for this imp, and check/store number this size was called (for current view) send this info as ext.pbsize @@ -484,7 +506,7 @@ const mapImpression = slot => { } const isVideoAd = bid => { - const xmlTester = new RegExp(/^<\?xml/); + const xmlTester = new RegExp(/^<\?xml| { return bid.admNative || (bid.adm && bid.adm.match(xmlTester)); } -const parseNative = (nativeData) => { +const parseNative = (nativeData, adUnitCode) => { const { link = {}, imptrackers: impressionTrackers, jstracker } = nativeData; const { url: clickUrl, clicktrackers: clickTrackers = [] } = link; + const macroReplacer = tracker => tracker.replace(new RegExp('%native_dom_id%', 'g'), adUnitCode); + let javascriptTrackers = isArray(jstracker) ? jstracker : jstracker && [jstracker]; + + // replace known macros in js trackers + javascriptTrackers = javascriptTrackers && javascriptTrackers.map(macroReplacer); const result = { clickUrl, clickTrackers, impressionTrackers, - javascriptTrackers: isArray(jstracker) ? jstracker : jstracker && [jstracker], + javascriptTrackers, }; nativeData.assets.forEach(asset => { @@ -627,6 +654,8 @@ const spec = { return true; }, buildRequests(validBidRequests, bidderRequest) { + logWarn('DEBUG: buildRequests', bidderRequest.auctionId, bidderRequest.bidderRequestId); + // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); @@ -644,7 +673,7 @@ const spec = { const ref = bidderRequest.refererInfo.ref; const payload = { - id: bidderRequest.auctionId, + id: bidderRequest.bidderRequestId, site: { id: siteId ? `${siteId}` : undefined, publisher: publisherId ? { id: publisherId } : undefined, @@ -669,6 +698,7 @@ const spec = { applyGdpr(bidderRequest, payload); applyClientHints(payload); applyUserIds(validBidRequests[0], payload); + applyTopics(bidderRequest, payload); return { method: 'POST', @@ -725,6 +755,7 @@ const spec = { if (bidRequest && site.id && !strIncludes(site.id, 'bidid')) { // found a matching request; add this bid + const { adUnitCode } = bidRequest; // store site data for future notification oneCodeDetection[bidId] = [site.id, site.slot]; @@ -760,7 +791,7 @@ const spec = { // check native object try { const nativeData = serverBid.admNative || JSON.parse(serverBid.adm).native; - bid.native = parseNative(nativeData); + bid.native = parseNative(nativeData, adUnitCode); bid.width = 1; bid.height = 1; } catch (err) { @@ -807,6 +838,15 @@ const spec = { } }, + onBidViewable(bid) { + const payload = getNotificationPayload(bid); + if (payload) { + payload.event = 'bidViewable'; + sendNotification(payload); + return payload; + } + }, + onBidWon(bid) { const payload = getNotificationPayload(bid); if (payload) { diff --git a/modules/stroeerCoreBidAdapter.js b/modules/stroeerCoreBidAdapter.js index 0737bec5a12..307a50c7f78 100644 --- a/modules/stroeerCoreBidAdapter.js +++ b/modules/stroeerCoreBidAdapter.js @@ -1,4 +1,4 @@ -import {buildUrl, deepAccess, getWindowSelf, getWindowTop, isEmpty, isStr, logWarn} from '../src/utils.js'; +import { buildUrl, deepAccess, generateUUID, getWindowSelf, getWindowTop, isEmpty, isStr, logWarn } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {find} from '../src/polyfill.js'; @@ -50,8 +50,7 @@ export const spec = { const refererInfo = bidderRequest.refererInfo; const basePayload = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - id: bidderRequest.auctionId, + id: generateUUID(), ref: refererInfo.ref, ssl: isSecureWindow(), mpa: isMainPageAccessible(), diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 8a42013b0be..86766200aaa 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -136,7 +136,7 @@ export const spec = { } }; - const url = [END_POINT_URL, publisherId].join('/'); + const url = END_POINT_URL + '?publisher=' + publisherId; return { url, diff --git a/modules/tapadIdSystem.js b/modules/tapadIdSystem.js index 149ba22eb92..2c24f062791 100644 --- a/modules/tapadIdSystem.js +++ b/modules/tapadIdSystem.js @@ -56,6 +56,12 @@ export const tapadIdSubmodule = { ); } } + }, + eids: { + 'tapadId': { + source: 'tapad.com', + atype: 1 + }, } } submodule('userId', tapadIdSubmodule); diff --git a/modules/tappxBidAdapter.js b/modules/tappxBidAdapter.js index 13a16224a79..90438f92e1b 100644 --- a/modules/tappxBidAdapter.js +++ b/modules/tappxBidAdapter.js @@ -12,7 +12,7 @@ const BIDDER_CODE = 'tappx'; const GVLID_CODE = 628; const TTL = 360; const CUR = 'USD'; -const TAPPX_BIDDER_VERSION = '0.1.2'; +const TAPPX_BIDDER_VERSION = '0.1.3'; const TYPE_CNN = 'prebidjs'; const LOG_PREFIX = '[TAPPX]: '; const VIDEO_SUPPORT = ['instream', 'outstream']; @@ -146,22 +146,22 @@ function validBasic(bid) { return false; } - if (bid.params.tappxkey == null) { + if (!bid.params.tappxkey) { logWarn(LOG_PREFIX, 'Please review the mandatory Tappxkey parameter.'); return false; } - if (bid.params.host == null) { + if (!bid.params.host) { logWarn(LOG_PREFIX, 'Please review the mandatory Host parameter.'); return false; } - let classicEndpoint = true + let classicEndpoint = true; if ((new RegExp(`^(vz.*|zz.*)\\.*$`, 'i')).test(bid.params.host)) { - classicEndpoint = false + classicEndpoint = false; } - if (classicEndpoint && bid.params.endpoint == null) { + if (classicEndpoint && !bid.params.endpoint) { logWarn(LOG_PREFIX, 'Please review the mandatory endpoint Tappx parameters.'); return false; } @@ -191,7 +191,7 @@ function validMediaType(bid) { */ function interpretBid(serverBid, request) { let bidReturned = { - requestId: request.bids.bidId, + requestId: request.bids?.bidId, cpm: serverBid.price, currency: serverBid.cur ? serverBid.cur : CUR, width: serverBid.w, @@ -206,7 +206,7 @@ function interpretBid(serverBid, request) { if (typeof serverBid.nurl != 'undefined') { bidReturned.nurl = serverBid.nurl } if (typeof serverBid.burl != 'undefined') { bidReturned.burl = serverBid.burl } - if (typeof request.bids.mediaTypes !== 'undefined' && typeof request.bids.mediaTypes.video !== 'undefined') { + if (typeof request.bids?.mediaTypes !== 'undefined' && typeof request.bids?.mediaTypes.video !== 'undefined') { bidReturned.vastXml = serverBid.adm; bidReturned.vastUrl = serverBid.lurl; bidReturned.ad = serverBid.adm; @@ -214,13 +214,12 @@ function interpretBid(serverBid, request) { bidReturned.width = serverBid.w; bidReturned.height = serverBid.h; - if (request.bids.mediaTypes.video.context === 'outstream') { - const url = (serverBid.ext.purl) ? serverBid.ext.purl : false; - if (typeof url === 'undefined') { + if (request.bids?.mediaTypes.video.context === 'outstream') { + if (!serverBid.ext.purl) { logWarn(LOG_PREFIX, 'Error getting player outstream from tappx'); return false; } - bidReturned.renderer = createRenderer(bidReturned, request, url); + bidReturned.renderer = createRenderer(bidReturned, request, serverBid.ext.purl); } } else { bidReturned.ad = serverBid.adm; @@ -228,7 +227,7 @@ function interpretBid(serverBid, request) { } if (typeof bidReturned.adomain !== 'undefined' || bidReturned.adomain !== null) { - bidReturned.meta = { advertiserDomains: request.bids.adomain }; + bidReturned.meta = { advertiserDomains: request.bids?.adomain }; } return bidReturned; @@ -252,6 +251,7 @@ function buildOneRequest(validBidRequests, bidderRequest) { const BIDEXTRA = deepAccess(validBidRequests, 'params.ext'); const bannerMediaType = deepAccess(validBidRequests, 'mediaTypes.banner'); const videoMediaType = deepAccess(validBidRequests, 'mediaTypes.video'); + const ORTB2 = deepAccess(validBidRequests, 'ortb2'); // let requests = []; let payload = {}; @@ -276,7 +276,11 @@ function buildOneRequest(validBidRequests, bidderRequest) { site.name = bundle; site.page = bidderRequest?.refererInfo?.page || deepAccess(validBidRequests, 'params.site.page') || bidderRequest?.refererInfo?.topmostLocation || window.location.href || bundle; site.domain = bundle; - site.ref = bidderRequest?.refererInfo?.ref || window.top.document.referrer || ''; + try { + site.ref = bidderRequest?.refererInfo?.ref || window.top.document.referrer || ''; + } catch (e) { + site.ref = bidderRequest?.refererInfo?.ref || window.document.referrer || ''; + } site.ext = {}; site.ext.is_amp = bidderRequest?.refererInfo?.isAmp || 0; site.ext.page_da = deepAccess(validBidRequests, 'params.site.page') || '-'; @@ -304,6 +308,8 @@ function buildOneRequest(validBidRequests, bidderRequest) { let h; if (bannerMediaType) { + if (!Array.isArray(bannerMediaType.sizes)) { logWarn(LOG_PREFIX, 'Banner sizes array not found.'); } + let banner = {}; w = bannerMediaType.sizes[0][0]; h = bannerMediaType.sizes[0][1]; @@ -341,7 +347,9 @@ function buildOneRequest(validBidRequests, bidderRequest) { } if ((video.w === undefined || video.w == null || video.w <= 0) || - (video.h === undefined || video.h == null || video.h <= 0)) { + (video.h === undefined || video.h == null || video.h <= 0)) { + if (!Array.isArray(videoMediaType.playerSize)) { logWarn(LOG_PREFIX, 'Video playerSize array not found.'); } + w = videoMediaType.playerSize[0][0]; h = videoMediaType.playerSize[0][1]; video.w = w; @@ -410,6 +418,14 @@ function buildOneRequest(validBidRequests, bidderRequest) { let geo = {}; geo.country = deepAccess(validBidRequests, 'params.geo.country'); // < Device object + let configGeo = {}; + configGeo.country = ORTB2?.device?.geo; + + if (typeof configGeo.country !== 'undefined') { + device.geo = configGeo; + } else if (typeof geo.country !== 'undefined') { + device.geo = geo; + }; // > GDPR let user = {}; @@ -491,7 +507,7 @@ function getLanguage() { function getOs() { let ua = navigator.userAgent; - if (ua.indexOf('Windows') != -1) { return 'Windows'; } else if (ua.indexOf('Mac OS X') != -1) { return 'macOS'; } else if (ua.match(/Android/)) { return 'Android'; } else if (ua.match(/(iPhone|iPod|iPad)/)) { return 'iOS'; } else if (ua.indexOf('Linux') != -1) { return 'Linux'; } else { return 'Unknown'; } + if (ua.match(/Android/)) { return 'Android'; } else if (ua.match(/(iPhone|iPod|iPad)/)) { return 'iOS'; } else if (ua.indexOf('Mac OS X') != -1) { return 'macOS'; } else if (ua.indexOf('Windows') != -1) { return 'Windows'; } else if (ua.indexOf('Linux') != -1) { return 'Linux'; } else { return 'Unknown'; } } function getVendor() { diff --git a/modules/tappxBidAdapter.md b/modules/tappxBidAdapter.md index 677718c261c..55f18531f28 100644 --- a/modules/tappxBidAdapter.md +++ b/modules/tappxBidAdapter.md @@ -92,3 +92,20 @@ Ads sizes available: [300,250], [320,50], [320,480], [480,320], [728,90], [768,1 } ]; ``` +### Configuration + +Use `setConfig` to configure this submodule ortb2.device.geo, this will allow geolocation +`Geo` object to bring First Party Information. + +```javascript +var TIMEOUT = 1000; +pbjs.setConfig({ + ortb2:{ + device:{ + geo:{ + country:'US' + } + } + } +}); +``` diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 6e1006ecf00..5e92af640b8 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -1,4 +1,4 @@ -import { getValue, logError, deepAccess, getBidIdParameter, parseSizesInput, isArray } from '../src/utils.js'; +import {getValue, logError, deepAccess, parseSizesInput, isArray, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -49,6 +49,8 @@ export const spec = { const payload = { referrer: getReferrerInfo(bidderRequest), pageReferrer: document.referrer, + pageTitle: getPageTitle().slice(0, 300), + pageDescription: getPageDescription().slice(0, 300), networkBandwidth: getConnectionDownLink(window.navigator), timeToFirstByte: getTimeToFirstByte(window), data: bids, @@ -170,6 +172,32 @@ function getReferrerInfo(bidderRequest) { return ref; } +function getPageTitle() { + try { + const ogTitle = window.top.document.querySelector('meta[property="og:title"]') + + return window.top.document.title || (ogTitle && ogTitle.content) || ''; + } catch (e) { + const ogTitle = document.querySelector('meta[property="og:title"]') + + return document.title || (ogTitle && ogTitle.content) || ''; + } +} + +function getPageDescription() { + let element; + + try { + element = window.top.document.querySelector('meta[name="description"]') || + window.top.document.querySelector('meta[property="og:description"]') + } catch (e) { + element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]') + } + + return (element && element.content) || ''; +} + function getConnectionDownLink(nav) { return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : ''; } @@ -220,6 +248,7 @@ function buildRequestObject(bid) { let placementId = getValue(bid.params, 'placementId'); let pageId = getValue(bid.params, 'pageId'); const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); + const videoPlcmt = deepAccess(bid, 'mediaTypes.video.plcmt'); reqObj.sizes = getSizes(bid); reqObj.bidId = getBidIdParameter('bidId', bid); @@ -229,6 +258,7 @@ function buildRequestObject(bid) { reqObj.adUnitCode = getBidIdParameter('adUnitCode', bid); reqObj.transactionId = bid.ortb2Imp?.ext?.tid || ''; if (gpid) { reqObj.gpid = gpid; } + if (videoPlcmt) { reqObj.videoPlcmt = videoPlcmt; } return reqObj; } diff --git a/modules/tncIdSystem.js b/modules/tncIdSystem.js index 24e3c79d4df..59254a9430f 100644 --- a/modules/tncIdSystem.js +++ b/modules/tncIdSystem.js @@ -57,6 +57,12 @@ export const tncidSubModule = { return { callback: function (cb) { return tncCallback(cb); } } + }, + eids: { + 'tncid': { + source: 'thenewco.it', + atype: 3 + }, } } diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index 4cb11af48fc..dffb8bc4860 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -1,24 +1,32 @@ -import {logError, logWarn, mergeDeep, isEmpty, safeJSONParse, logInfo, hasDeviceAccess} from '../src/utils.js'; +import {isEmpty, logError, logWarn, mergeDeep, safeJSONParse} from '../src/utils.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {submodule} from '../src/hook.js'; import {GreedyPromise} from '../src/utils/promise.js'; import {config} from '../src/config.js'; import {getCoreStorageManager} from '../src/storageManager.js'; import {includes} from '../src/polyfill.js'; -import {gdprDataHandler} from '../src/adapterManager.js'; +import {isActivityAllowed} from '../src/activities/rules.js'; +import {ACTIVITY_ENRICH_UFPD} from '../src/activities/activities.js'; +import {activityParams} from '../src/activities/activityParams.js'; +import {MODULE_TYPE_BIDDER} from '../src/activities/modules.js'; const MODULE_NAME = 'topicsFpd'; const DEFAULT_EXPIRATION_DAYS = 21; -const TCF_REQUIRED_PURPOSES = ['1', '2', '3', '4']; -let HAS_GDPR_CONSENT = true; +const DEFAULT_FETCH_RATE_IN_DAYS = 1; let LOAD_TOPICS_INITIALISE = false; -const HAS_DEVICE_ACCESS = hasDeviceAccess(); + +export function reset() { + LOAD_TOPICS_INITIALISE = false; +} const bidderIframeList = { - maxTopicCaller: 1, + maxTopicCaller: 2, bidders: [{ bidder: 'pubmatic', iframeURL: 'https://ads.pubmatic.com/AdServer/js/topics/topics_frame.html' + }, { + bidder: 'rtbhouse', + iframeURL: 'https://topics.authorizedvault.com/topicsapi.html' }] } export const coreStorage = getCoreStorageManager(MODULE_NAME); @@ -78,15 +86,21 @@ export function getTopicsData(name, topics, taxonomies = TAXONOMIES) { if (name != null) { datum.name = name; } + return datum; }) ); } +function isTopicsSupported(doc = document) { + return 'browsingTopics' in doc && doc.featurePolicy.allowsFeature('browsing-topics') +} + export function getTopics(doc = document) { let topics = null; + try { - if ('browsingTopics' in doc && doc.featurePolicy.allowsFeature('browsing-topics')) { + if (isTopicsSupported(doc)) { topics = GreedyPromise.resolve(doc.browsingTopics()); } } catch (e) { @@ -95,6 +109,7 @@ export function getTopics(doc = document) { if (topics == null) { topics = GreedyPromise.resolve([]); } + return topics; } @@ -123,19 +138,16 @@ export function processFpd(config, {global}, {data = topicsData} = {}) { */ export function getCachedTopics() { let cachedTopicData = []; - if (!HAS_GDPR_CONSENT || !HAS_DEVICE_ACCESS) { - return cachedTopicData; - } const topics = config.getConfig('userSync.topics') || bidderIframeList; const bidderList = topics.bidders || []; let storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName))); storedSegments && storedSegments.forEach((value, cachedBidder) => { // Check bidder exist in config for cached bidder data and then only retrieve the cached data - let bidderConfigObj = bidderList.find(({bidder}) => cachedBidder == bidder) - if (bidderConfigObj) { + let bidderConfigObj = bidderList.find(({bidder}) => cachedBidder === bidder) + if (bidderConfigObj && isActivityAllowed(ACTIVITY_ENRICH_UFPD, activityParams(MODULE_TYPE_BIDDER, cachedBidder))) { if (!isCachedDataExpired(value[lastUpdated], bidderConfigObj?.expiry || DEFAULT_EXPIRATION_DAYS)) { Object.keys(value).forEach((segData) => { - segData != lastUpdated && cachedTopicData.push(value[segData]); + segData !== lastUpdated && cachedTopicData.push(value[segData]); }) } else { // delete the specific bidder map from the store and store the updated maps @@ -148,7 +160,7 @@ export function getCachedTopics() { } /** - * Recieve messages from iframe loaded for bidders to fetch topic + * Receive messages from iframe loaded for bidders to fetch topic * @param {MessageEvent} evt */ export function receiveMessage(evt) { @@ -157,7 +169,7 @@ export function receiveMessage(evt) { let data = safeJSONParse(evt.data); if (includes(getLoadedIframeURL(), evt.origin) && data && data.segment && !isEmpty(data.segment.topics)) { const {domain, topics, bidder} = data.segment; - const iframeTopicsData = getTopicsData(domain, topics)[0]; + const iframeTopicsData = getTopicsData(domain, topics); iframeTopicsData && storeInLocalStorage(bidder, iframeTopicsData); } } catch (err) { } @@ -165,18 +177,20 @@ export function receiveMessage(evt) { } /** -Function to store Topics data recieved from iframe in storage(name: "prebid:topics") +Function to store Topics data received from iframe in storage(name: "prebid:topics") * @param {Topics} topics */ export function storeInLocalStorage(bidder, topics) { const storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName))); - if (storedSegments.has(bidder)) { - storedSegments.get(bidder)[topics['ext']['segclass']] = topics; - storedSegments.get(bidder)[lastUpdated] = new Date().getTime(); - storedSegments.set(bidder, storedSegments.get(bidder)); - } else { - storedSegments.set(bidder, {[topics.ext.segclass]: topics, [lastUpdated]: new Date().getTime()}) - } + const topicsObj = { + [lastUpdated]: new Date().getTime() + }; + + topics.forEach((topic) => { + topicsObj[topic.ext.segclass] = topic; + }); + + storedSegments.set(bidder, topicsObj); coreStorage.setDataInLocalStorage(topicStorageName, JSON.stringify([...storedSegments])); } @@ -201,55 +215,43 @@ function listenMessagesFromTopicIframe() { window.addEventListener('message', receiveMessage, false); } -function checkTCFv2(vendorData, requiredPurposes = TCF_REQUIRED_PURPOSES) { - const {gdprApplies, purpose} = vendorData; - if (!gdprApplies || !purpose) { - return true; - } - return requiredPurposes.map((purposeNo) => { - const purposeConsent = purpose.consents ? purpose.consents[purposeNo] : false; - if (purposeConsent) { - return true; - } - return false; - }).reduce((a, b) => a && b, true); -} - -export function hasGDPRConsent() { - // Check for GDPR consent for purpose 1,2,3,4 and return false if consent has not been given - const gdprConsent = gdprDataHandler.getConsentData(); - const hasGdpr = (gdprConsent && typeof gdprConsent.gdprApplies === 'boolean' && gdprConsent.gdprApplies) ? 1 : 0; - const gdprConsentString = hasGdpr ? gdprConsent.consentString : ''; - if (hasGdpr) { - if ((!gdprConsentString || gdprConsentString === '') || !gdprConsent.vendorData) { - return false; - } - return checkTCFv2(gdprConsent.vendorData); - } - return true; -} - /** * function to load the iframes of the bidder to load the topics data */ -function loadTopicsForBidders() { - HAS_GDPR_CONSENT = hasGDPRConsent(); - if (!HAS_GDPR_CONSENT || !HAS_DEVICE_ACCESS) { - logInfo('Topics Module : Consent string is required to fetch the topics from third party domains.'); - return; - } +export function loadTopicsForBidders(doc = document) { + if (!isTopicsSupported(doc)) return; const topics = config.getConfig('userSync.topics') || bidderIframeList; + if (topics) { listenMessagesFromTopicIframe(); const randomBidders = getRandomBidders(topics.bidders || [], topics.maxTopicCaller || 1) - randomBidders && randomBidders.forEach(({ bidder, iframeURL }) => { + randomBidders && randomBidders.forEach(({ bidder, iframeURL, fetchUrl, fetchRate }) => { if (bidder && iframeURL) { - let ifrm = document.createElement('iframe'); + let ifrm = doc.createElement('iframe'); ifrm.name = 'ifrm_'.concat(bidder); ifrm.src = ''.concat(iframeURL, '?bidder=').concat(bidder); ifrm.style.display = 'none'; setLoadedIframeURL(new URL(iframeURL).origin); - iframeURL && window.document.documentElement.appendChild(ifrm); + iframeURL && doc.documentElement.appendChild(ifrm); + } + + if (bidder && fetchUrl) { + let storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName))); + const bidderLsEntry = storedSegments.get(bidder); + + if (!bidderLsEntry || (bidderLsEntry && isCachedDataExpired(bidderLsEntry[lastUpdated], fetchRate || DEFAULT_FETCH_RATE_IN_DAYS))) { + window.fetch(`${fetchUrl}?bidder=${bidder}`, {browsingTopics: true}) + .then(response => { + return response.json(); + }) + .then(data => { + if (data && data.segment && !isEmpty(data.segment.topics)) { + const {domain, topics, bidder} = data.segment; + const fetchTopicsData = getTopicsData(domain, topics); + fetchTopicsData && storeInLocalStorage(bidder, fetchTopicsData); + } + }); + } } }) } else { diff --git a/modules/topicsFpdModule.md b/modules/topicsFpdModule.md index b1bc3cb0d5b..6ef0bf241dd 100644 --- a/modules/topicsFpdModule.md +++ b/modules/topicsFpdModule.md @@ -36,6 +36,10 @@ pbjs.setConfig({ bidder: 'pubmatic', iframeURL: 'https://ads.pubmatic.com/AdServer/js/topics/topics_frame.html', expiry: 7 // Configurable expiry days + },{ + bidder: 'rtbhouse', + iframeURL: 'https://topics.authorizedvault.com/topicsapi.html', + expiry: 7 // Configurable expiry days },{ bidder: 'rubicon', iframeURL: 'https://rubicon.com:8080/topics/fpd/topic.html', // dummy URL diff --git a/modules/tpmnBidAdapter.js b/modules/tpmnBidAdapter.js index bac99d578c5..61f8a4c98af 100644 --- a/modules/tpmnBidAdapter.js +++ b/modules/tpmnBidAdapter.js @@ -1,164 +1,242 @@ /* eslint-disable no-tabs */ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { parseUrl, deepAccess } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { getStorageManager } from '../src/storageManager.js'; -import { BANNER } from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; import { config } from '../src/config.js'; +import * as utils from '../src/utils.js'; -export const ADAPTER_VERSION = '1'; -const SUPPORTED_AD_TYPES = [BANNER]; const BIDDER_CODE = 'tpmn'; -const URL = 'https://ad.tpmn.co.kr/prebidhb.tpmn'; -const IFRAMESYNC = 'https://ad.tpmn.co.kr/sync.tpmn?type=iframe'; -export const storage = getStorageManager({bidderCode: BIDDER_CODE}); +const DEFAULT_BID_TTL = 500; +const DEFAULT_CURRENCY = 'USD'; +const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; +// const BIDDER_ENDPOINT_URL = 'http://localhost:8081/ortb/pbjs_bidder'; +const BIDDER_ENDPOINT_URL = 'https://gat.tpmn.io/ortb/pbjs_bidder'; +const IFRAMESYNC = 'https://gat.tpmn.io/sync/iframe'; +const COMMON_PARAMS = [ + 'battr' +]; +export const VIDEO_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; +export const ADAPTER_VERSION = '2.0'; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, /** - *Determines whether or not the given bid request is valid. - * - * @param {object} bid The bid to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: function (bid) { - return 'params' in bid && - 'inventoryId' in bid.params && - 'publisherId' in bid.params && - !isNaN(Number(bid.params.inventoryId)) && - bid.params.inventoryId > 0 && - (typeof bid.mediaTypes.banner.sizes != 'undefined'); // only accepting appropriate sizes - }, - - /** - * @param {BidRequest[]} bidRequests - * @param {*} bidderRequest - * @return {ServerRequest} + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} bid The bid that won the auction */ - buildRequests: (bidRequests, bidderRequest) => { - if (bidRequests.length === 0) { - return []; + onBidWon: function (bid) { + if (bid.burl) { + utils.triggerPixel(bid.burl); } - const bids = bidRequests.map(bidToRequest); - const bidderApiUrl = URL; - const payload = { - 'bids': [...bids], - 'site': createSite(bidderRequest.refererInfo) - }; - return [{ - method: 'POST', - url: bidderApiUrl, - data: payload - }]; + } +} + +function isBidRequestValid(bid) { + return (isValidInventoryId(bid) && (isValidBannerRequest(bid) || isValidVideoRequest(bid))); +} + +function isValidInventoryId(bid) { + return 'params' in bid && 'inventoryId' in bid.params && utils.isNumber(bid.params.inventoryId); +} + +function isValidBannerRequest(bid) { + const bannerSizes = utils.deepAccess(bid, `mediaTypes.${BANNER}.sizes`); + return utils.isArray(bannerSizes) && bannerSizes.length > 0 && bannerSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1])); +} + +function isValidVideoRequest(bid) { + const videoSizes = utils.deepAccess(bid, `mediaTypes.${VIDEO}.playerSize`); + const videoMimes = utils.deepAccess(bid, `mediaTypes.${VIDEO}.mimes`); + + const isValidVideoSize = utils.isArray(videoSizes) && videoSizes.length > 0 && videoSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1])); + const isValidVideoMimes = utils.isArray(videoMimes) && videoMimes.length > 0; + return isValidVideoSize && isValidVideoMimes; +} + +function buildRequests(validBidRequests, bidderRequest) { + let requests = []; + try { + if (validBidRequests.length === 0 || !bidderRequest) return []; + let bannerBids = validBidRequests.filter(bid => utils.deepAccess(bid, 'mediaTypes.banner')); + let videoBids = validBidRequests.filter(bid => utils.deepAccess(bid, 'mediaTypes.video')); + + bannerBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, BANNER)); + }); + + videoBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + } catch (err) { + utils.logWarn('buildRequests', err); + } + + return requests; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + const rtbData = CONVERTER.toORTB({ bidRequests, bidderRequest, context: { mediaType } }) + + const bid = bidRequests.find((b) => b.params.inventoryId) + + if (bid.params.inventoryId) rtbData.ext = {}; + if (bid.params.inventoryId) rtbData.ext.inventoryId = bid.params.inventoryId + + const ortb2Data = bidderRequest?.ortb2 || {}; + const bcat = ortb2Data?.bcat || bid.params.bcat || []; + const badv = ortb2Data?.badv || bid.params.badv || []; + const bapp = ortb2Data?.bapp || bid.params.bapp || []; + + if (bcat.length > 0) { + rtbData.bcat = bcat; + } + if (badv.length > 0) { + rtbData.badv = badv; + } + if (badv.length > 0) { + rtbData.bapp = bapp; + } + + return { + method: 'POST', + url: BIDDER_ENDPOINT_URL + '?v=' + ADAPTER_VERSION, + data: rtbData, + options: { contentType: 'application/json;charset=UTF-8', withCredentials: false } + } +} + +function interpretResponse(response, request) { + return CONVERTER.fromORTB({ request: request.data, response: response.body }).bids; +} + +registerBidder(spec); + +const CONVERTER = ortbConverter({ + context: { + netRevenue: true, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY }, - /** - * Unpack the response from the server into a list of bids. - * - * @param {serverResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - interpretResponse: function (serverResponse, serverRequest) { - if (!Array.isArray(serverResponse.body)) { - return []; + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + if (!imp.bidfloor && bidRequest.params.bidFloor) { + imp.bidfloor = bidRequest.params.bidFloor; } - // server response body is an array of bid results - const bidResults = serverResponse.body; - // our server directly returns the format needed by prebid.js so no more - // transformation is needed here. - return bidResults; - }, - - getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { - const syncArr = []; - if (syncOptions.iframeEnabled) { - let policyParam = ''; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - policyParam += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - policyParam += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + [VIDEO, BANNER].forEach(namespace => { + COMMON_PARAMS.forEach(param => { + if (bidRequest.params.hasOwnProperty(param)) { + utils.deepSetValue(imp, `${namespace}.${param}`, bidRequest.params[param]) } + }) + }) + return imp; + }, + bidResponse(buildBidResponse, bid, context) { + const {bidRequest} = context; + const bidResponse = buildBidResponse(bid, context); + if (bidResponse.mediaType === BANNER) { + bidResponse.ad = bid.adm; + } else if (bidResponse.mediaType === VIDEO) { + if (bidRequest.mediaTypes.video.context === 'outstream') { + bidResponse.rendererUrl = VIDEO_RENDERER_URL; + bidResponse.renderer = createRenderer(bidRequest); } - if (uspConsent && uspConsent.consentString) { - policyParam += `&ccpa_consent=${uspConsent.consentString}`; - } - const coppa = config.getConfig('coppa') ? 1 : 0; - policyParam += `&coppa=${coppa}`; - syncArr.push({ - type: 'iframe', - url: IFRAMESYNC + policyParam - }); - } else { - syncArr.push({ - type: 'image', - url: 'https://x.bidswitch.net/sync?ssp=tpmn' - }); - syncArr.push({ - type: 'image', - url: 'https://gocm.c.appier.net/tpmn' - }); - syncArr.push({ - type: 'image', - url: 'https://info.mmnneo.com/getGuidRedirect.info?url=https%3A%2F%2Fad.tpmn.co.kr%2Fcookiesync.tpmn%3Ftpmn_nid%3Dbf91e8b3b9d3f1af3fc1d657f090b4fb%26tpmn_buid%3D' - }); - syncArr.push({ - type: 'image', - url: 'https://sync.aralego.com/idSync?redirect=https%3A%2F%2Fad.tpmn.co.kr%2FpixelCt.tpmn%3Ftpmn_nid%3Dde91e8b3b9d3f1af3fc1d657f090b815%26tpmn_buid%3DSspCookieUserId' - }); } - return syncArr; + return bidResponse; }, -}; + overrides: { + imp: { + video(orig, imp, bidRequest, context) { + let videoParams = bidRequest.mediaTypes[VIDEO]; + if (videoParams) { + videoParams = Object.assign({}, videoParams, bidRequest.params.video); + bidRequest = {...bidRequest, mediaTypes: {[VIDEO]: videoParams}} + } + orig(imp, bidRequest, context); + }, + }, + } +}); -registerBidder(spec); +function createRenderer(bid) { + const renderer = Renderer.install({ + id: bid.bidId, + url: VIDEO_RENDERER_URL, + config: utils.deepAccess(bid, 'renderer.options'), + loaded: false, + adUnitCode: bid.adUnitCode + }); -/** - * Creates site description object - */ -function createSite(refInfo) { - let url = parseUrl(refInfo.page || ''); - let site = { - 'domain': url.hostname, - 'page': url.protocol + '://' + url.hostname + url.pathname - }; - if (refInfo.ref) { - site.ref = refInfo.ref - } - let keywords = document.getElementsByTagName('meta')['keywords']; - if (keywords && keywords.content) { - site.keywords = keywords.content; + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); } - return site; + return renderer; } -function parseSize(size) { - let sizeObj = {} - sizeObj.width = parseInt(size[0], 10); - sizeObj.height = parseInt(size[1], 10); - return sizeObj; -} +function outstreamRender(bid, doc) { + bid.renderer.push(() => { + const win = utils.getWindowFromDocument(doc) || window; + win.ANOutstreamVideo.renderAd({ + sizes: [bid.playerWidth, bid.playerHeight], + targetId: bid.adUnitCode, + rendererOptions: bid.renderer.getConfig(), + adResponse: { content: bid.vastXml } -function parseSizes(sizes) { - if (Array.isArray(sizes[0])) { // is there several sizes ? (ie. [[728,90],[200,300]]) - return sizes.map(size => parseSize(size)); - } - return [parseSize(sizes)]; // or a single one ? (ie. [728,90]) + }, handleOutstreamRendererEvents.bind(null, bid)); + }); } -function getBannerSizes(bidRequest) { - return parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes') || bidRequest.sizes); +function handleOutstreamRendererEvents(bid, id, eventName) { + bid.renderer.handleVideoEvent({ id, eventName }); } -function bidToRequest(bid) { - const bidObj = {}; - bidObj.sizes = getBannerSizes(bid); - - bidObj.inventoryId = bid.params.inventoryId; - bidObj.publisherId = bid.params.publisherId; - bidObj.bidId = bid.bidId; - bidObj.adUnitCode = bid.adUnitCode; - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - bidObj.auctionId = bid.auctionId; - - return bidObj; +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncArr = []; + if (syncOptions.iframeEnabled) { + let policyParam = ''; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + policyParam += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + policyParam += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + policyParam += `&ccpa_consent=${uspConsent.consentString}`; + } + const coppa = config.getConfig('coppa') ? 1 : 0; + policyParam += `&coppa=${coppa}`; + syncArr.push({ + type: 'iframe', + url: IFRAMESYNC + '?' + policyParam + }); + } else { + syncArr.push({ + type: 'image', + url: 'https://x.bidswitch.net/sync?ssp=tpmn' + }); + syncArr.push({ + type: 'image', + url: 'https://gocm.c.appier.net/tpmn' + }); + syncArr.push({ + type: 'image', + url: 'https://info.mmnneo.com/getGuidRedirect.info?url=https%3A%2F%2Fad.tpmn.co.kr%2Fcookiesync.tpmn%3Ftpmn_nid%3Dbf91e8b3b9d3f1af3fc1d657f090b4fb%26tpmn_buid%3D' + }); + syncArr.push({ + type: 'image', + url: 'https://sync.aralego.com/idSync?redirect=https%3A%2F%2Fad.tpmn.co.kr%2FpixelCt.tpmn%3Ftpmn_nid%3Dde91e8b3b9d3f1af3fc1d657f090b815%26tpmn_buid%3DSspCookieUserId' + }); + } + return syncArr; } diff --git a/modules/tpmnBidAdapter.md b/modules/tpmnBidAdapter.md index 8387528bb0f..3b016d7e5b2 100644 --- a/modules/tpmnBidAdapter.md +++ b/modules/tpmnBidAdapter.md @@ -11,10 +11,27 @@ Maintainer: develop@tpmn.co.kr Connects to TPMN exchange for bids. NOTE: -- TPMN bid adapter only supports Banner at the moment. +- TPMN bid adapter only supports MediaType BANNER, VIDEO. - Multi-currency is not supported. +- Please contact the TPMN sales team via email for "inventoryId" issuance. -# Sample Ad Unit Config + +# Bid Parameters + +## bids.params (Banner, Video) +***Pay attention to the case sensitivity.*** + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| -------------- | ----------- | ------------------------------------------ | ------------- | ------------ | +| `inventoryId` | required | Ad Inventory id TPMN | 123 | Number | +| `bidFloor` | recommended | Minimum price in USD. bidFloor applies to a specific unit. | 1.50 | Number | +| `bcat` | optional | IAB 5.1 Content Categories | ['IAB7-39'] | [String] | +| `badv` | optional | IAB Block list of advertisers by their domains | ['example.com'] | [String] | +| `bapp` | optional | IAB Block list of applications | ['com.blocked'] | [String] | + + +# Banner Ad Unit Config ``` var adUnits = [{ // Banner adUnit @@ -22,16 +39,77 @@ NOTE: mediaTypes: { banner: { sizes: [[300, 250], [320, 50]], // banner size + battr: [1,2,3] // optional } }, bids: [ { bidder: 'tpmn', params: { - inventoryId: '1', - publisherId: 'TPMN' + inventoryId: 1, // required + bidFloor: 2.0, // recommended + ... // bcat, badv, bapp // optional } } ] }]; +``` + + +# mediaTypes Parameters + +## mediaTypes.banner + +The following banner parameters are supported here so publishers may fully declare their banner inventory: + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| --------- | ------------| ----------------------------------------------------------------- | --------- | --------- | +| `sizes` | required | Avalaible sizes supported for banner ad unit | [ [300, 250], [300, 600] ] | [[Integer, Integer], [Integer, Integer]] | +| `battr` | optional | IAB 5.3 Creative Attributes | [1,2,3] | [Number] | +## mediaTypes.video + +We support the following OpenRTB params that can be specified in `mediaTypes.video` or in `bids[].params.video` + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| --------- | ------------| ----------------------------------------------------------------- | --------- | --------- | +| `context` | required | instream or outstream |'outstream' | string | +| `playerSize` | required | Avalaible sizes supported for video ad unit. | [[300, 250]] | [Integer, Integer] | +| `mimes` | required | List of content MIME types supported by the player. | ['video/mp4']| [String]| +| `protocols` | optional | Supported video bid response protocol values. | [2,3,5,6] | [integers]| +| `api` | optional | Supported API framework values. | [2] | [integers] | +| `maxduration` | optional | Maximum video ad duration in seconds. | 30 | Integer | +| `minduration` | optional | Minimum video ad duration in seconds. | 6 | Integer | +| `startdelay` | optional | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | 0 | Integer | +| `placement` | optional | Placement type for the impression. | 1 | Integer | +| `minbitrate` | optional | Minimum bit rate in Kbps. | 300 | Integer | +| `maxbitrate` | optional | Maximum bit rate in Kbps. | 9600 | Integer | +| `playbackmethod` | optional | Playback methods that may be in use. Only one method is typically used in practice. | [2]| [Integers] | +| `linearity` | optional | OpenRTB2 linearity. in-strea,overlay... | 1 | Integer | +| `skip` | optional | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes . | 1 | Integer | +| `battr` | optional | IAB 5.3 Creative Attributes | [1,2,3] | [Number] | + + +# Video Ad Unit Config +``` + var adUnits = [{ + code: 'video-div', + mediaTypes: { + video: { + context: 'instream', // required + mimes: ['video/mp4'], // required + playerSize: [[ 640, 480 ]], // required + ... // skippable, startdelay, battr.. // optional + } + }, + bids: [{ + bidder: 'tpmn', + params: { + inventoryId: 2, // required + bidFloor: 2.0, // recommended + ... // bcat, badv, bapp // optional + } + }] + }]; ``` \ No newline at end of file diff --git a/modules/trafficgateBidAdapter.js b/modules/trafficgateBidAdapter.js index 6665c397fe9..fcd84306099 100644 --- a/modules/trafficgateBidAdapter.js +++ b/modules/trafficgateBidAdapter.js @@ -1,95 +1,150 @@ -import { getWindowLocation, deepAccess } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {deepAccess, mergeDeep} from '../src/utils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'trafficgate'; -const URL = 'https://[HOST].bc-plugin.com/?c=o&m=multi' +const URL = 'https://[HOST].bc-plugin.com/prebidjs' export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO, NATIVE], + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + transformBidParams, + isBannerBid +}; - isBidRequestValid: function (bid) { - return !!(bid.bidId && bid.params && parseInt(bid.params.placementId) && bid.params.host) - }, - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: function (validBidRequests) { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - if (validBidRequests && validBidRequests.length === 0) return []; - - const location = getWindowLocation() - const placements = [] - - validBidRequests.forEach((bidReq) => { - placements.push({ - placementId: bidReq.params.placementId, - bidId: bidReq.bidId, - traffic: getMediatype(bidReq) - }) - }) +registerBidder(spec) - return { - method: 'POST', - url: URL.replace('[HOST]', validBidRequests[0].params.host), - data: { - language: (navigator && navigator.language) ? navigator.language : '', - secure: +(location.protocol === 'https:'), - host: location.hostname, - page: location.pathname, - placements: placements +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300 + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + mergeDeep(imp, { + ext: { + bidder: { + placementId: bidRequest.params.placementId, + host: bidRequest.params.host + } } + }); + if (bidRequest.params.customFloor && !imp.bidfloor) { + imp.bidfloor = bidRequest.params.customFloor; } + return imp; }, - - interpretResponse: function (opts) { - const body = opts.body - const response = [] - - for (let i = 0; i < body.length; i++) { - const item = body[i] - if (isBidResponseValid(item)) { - response.push(item) + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + mergeDeep(req, { + at: 1, + }) + const bid = context.bidRequests[0]; + if (bid.params.test) { + req.test = 1 + } + return req; + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + if (bid.ext) { + bidResponse.meta.networkId = bid.ext.networkId; + bidResponse.meta.advertiserDomains = bid.ext.advertiserDomains; + } + return bidResponse; + }, + response(buildResponse, bidResponses, ortbResponse, context) { + const response = buildResponse(bidResponses, ortbResponse, context); + return response.bids + }, + overrides: { + imp: { + bidfloor(setBidFloor, imp, bidRequest, context) { + const floor = {}; + setBidFloor(floor, bidRequest, {...context, currency: 'USD'}); + if (floor.bidfloorcur === 'USD') { + Object.assign(imp, floor); + } + }, + video(orig, imp, bidRequest, context) { + if (FEATURES.VIDEO) { + let videoParams = bidRequest.mediaTypes[VIDEO]; + if (videoParams) { + videoParams = Object.assign({}, videoParams, bidRequest.params.video); + bidRequest = {...bidRequest, mediaTypes: {[VIDEO]: videoParams}} + } + orig(imp, bidRequest, context); + if (imp.video && videoParams?.context === 'outstream') { + imp.video.placement = imp.video.placement || 4; + } + } } } - - return response } -} +}); -registerBidder(spec) +function transformBidParams(params, isOpenRtb) { + return convertTypes({ + 'customFloor': 'number', + 'placementId': 'number', + 'host': 'string' + }, params); +} -function isBidResponseValid (bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { +function isBidRequestValid(bidRequest) { + const isValid = bidRequest.params.placementId && bidRequest.params.host; + if (!isValid) { return false } - switch (bid['mediaType']) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad) - case VIDEO: - return Boolean(bid.vastUrl) - case NATIVE: - return Boolean(bid.title && bid.image && bid.impressionTrackers) - default: - return false + if (isBannerBid(bidRequest)) { + return deepAccess(bidRequest, 'mediaTypes.banner.sizes.length') > 0; } + return true } -function getMediatype(bidRequest) { - if (deepAccess(bidRequest, 'mediaTypes.banner')) { - return BANNER; - } - if (deepAccess(bidRequest, 'mediaTypes.video')) { - return VIDEO; +function buildRequests(bids, bidderRequest) { + let videoBids = bids.filter(bid => isVideoBid(bid)); + let bannerBids = bids.filter(bid => isBannerBid(bid)); + let requests = bannerBids.length ? [createRequest(bannerBids, bidderRequest, BANNER)] : []; + videoBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + return requests; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + return { + method: 'POST', + url: URL.replace('[HOST]', bidRequests[0].params.host), + data: converter.toORTB({bidRequests, bidderRequest, context: {mediaType}}) } - if (deepAccess(bidRequest, 'mediaTypes.native')) { - return NATIVE; +} + +function isVideoBid(bid) { + return !!deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return !!deepAccess(bid, 'mediaTypes.banner'); +} + +function interpretResponse(resp, req) { + if (!resp.body) { + resp.body = {nbr: 0}; } + return converter.fromORTB({request: req.data, response: resp.body}); +} + +export const spec2 = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid: function (bid) { + return !!(bid.bidId && bid.params && parseInt(bid.params.placementId) && bid.params.host) + }, } diff --git a/modules/trionBidAdapter.js b/modules/trionBidAdapter.js index b6375243a5a..e976396c71c 100644 --- a/modules/trionBidAdapter.js +++ b/modules/trionBidAdapter.js @@ -1,6 +1,7 @@ -import { getBidIdParameter, parseSizesInput, tryAppendQueryString } from '../src/utils.js'; +import {getBidIdParameter, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BID_REQUEST_BASE_URL = 'https://in-appadvertising.com/api/bidRequest'; const USER_SYNC_URL = 'https://in-appadvertising.com/api/userSync.html'; diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index df00495b88b..bfbf1409c1b 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -1,8 +1,9 @@ -import { tryAppendQueryString, logMessage, logError, isEmpty, isStr, isPlainObject, isArray, logWarn } from '../src/utils.js'; +import { logMessage, logError, isEmpty, isStr, isPlainObject, isArray, logWarn } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const GVLID = 28; const BIDDER_CODE = 'triplelift'; @@ -56,6 +57,10 @@ export const tripleliftAdapterSpec = { tlCall = tryAppendQueryString(tlCall, 'us_privacy', bidderRequest.uspConsent); } + if (bidderRequest && bidderRequest.fledgeEnabled) { + tlCall = tryAppendQueryString(tlCall, 'fledge', bidderRequest.fledgeEnabled); + } + if (config.getConfig('coppa') === true) { tlCall = tryAppendQueryString(tlCall, 'coppa', true); } @@ -75,12 +80,29 @@ export const tripleliftAdapterSpec = { interpretResponse: function(serverResponse, {bidderRequest}) { let bids = serverResponse.body.bids || []; - return bids.map(function(bid) { - return _buildResponseObject(bidderRequest, bid); - }); + const paapi = serverResponse.body.paapi || []; + + bids = bids.map(bid => _buildResponseObject(bidderRequest, bid)); + + if (paapi.length > 0) { + const fledgeAuctionConfigs = paapi.map(config => { + return { + bidId: bidderRequest.bids[config.imp_id].bidId, + config: config.auctionConfig + }; + }); + + logMessage('Response with FLEDGE:', { bids, fledgeAuctionConfigs }); + return { + bids, + fledgeAuctionConfigs + }; + } else { + return bids; + } }, - getUserSyncs: function(syncOptions, responses, gdprConsent, usPrivacy) { + getUserSyncs: function(syncOptions, responses, gdprConsent, usPrivacy, gppConsent) { let syncType = _getSyncType(syncOptions); if (!syncType) return; @@ -100,6 +122,15 @@ export const tripleliftAdapterSpec = { syncEndpoint = tryAppendQueryString(syncEndpoint, 'us_privacy', usPrivacy); } + if (gppConsent) { + if (gppConsent.gppString) { + syncEndpoint = tryAppendQueryString(syncEndpoint, 'gpp', gppConsent.gppString); + } + if (gppConsent.applicableSections && gppConsent.applicableSections.length !== 0) { + syncEndpoint = tryAppendQueryString(syncEndpoint, 'gpp_sid', _filterSid(gppConsent.applicableSections)); + } + } + return [{ type: syncType, url: syncEndpoint @@ -113,6 +144,13 @@ function _getSyncType(syncOptions) { if (syncOptions.pixelEnabled) return 'image'; } +function _filterSid(sid) { + return sid.filter(element => { + return Number.isInteger(element); + }) + .join(','); +} + function _buildPostBody(bidRequests, bidderRequest) { let data = {}; let { schain } = bidRequests[0]; @@ -169,6 +207,11 @@ function _buildPostBody(bidRequests, bidderRequest) { if (bidderRequest?.ortb2?.regs?.gpp) { data.regs = Object.assign({}, bidderRequest.ortb2.regs); } + + if (bidderRequest?.ortb2) { + data.ext.ortb2 = Object.assign({}, bidderRequest.ortb2); + } + return data; } @@ -197,7 +240,12 @@ function _getORTBVideo(bidRequest) { } catch (err) { logWarn('Video size not defined', err); } - if (video.context === 'instream') video.placement = 1; + // honor existing publisher settings + if (video.context === 'instream') { + if (!video.placement) { + video.placement = 1; + } + } if (video.context === 'outstream') { if (!video.placement) { video.placement = 3 diff --git a/modules/ttdBidAdapter.js b/modules/ttdBidAdapter.js index bbe207abc9e..17a3cd652e8 100644 --- a/modules/ttdBidAdapter.js +++ b/modules/ttdBidAdapter.js @@ -4,7 +4,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import {isNumber} from '../src/utils.js'; -const BIDADAPTERVERSION = 'TTD-PREBID-2022.06.28'; +const BIDADAPTERVERSION = 'TTD-PREBID-2023.09.05'; const BIDDER_CODE = 'ttd'; const BIDDER_CODE_LONG = 'thetradedesk'; const BIDDER_ENDPOINT = 'https://direct.adsrvr.org/bid/bidder/'; @@ -406,7 +406,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { const firstPartyData = bidderRequest.ortb2 || {}; let topLevel = { - id: bidderRequest.auctionId, + id: bidderRequest.bidderRequestId, imp: validBidRequests.map(bidRequest => getImpression(bidRequest)), site: getSite(bidderRequest, firstPartyData), device: getDevice(firstPartyData), diff --git a/modules/uid2IdSystem.js b/modules/uid2IdSystem.js index f8178364e58..b9b3dfa2380 100644 --- a/modules/uid2IdSystem.js +++ b/modules/uid2IdSystem.js @@ -7,13 +7,14 @@ */ import { logInfo, logWarn } from '../src/utils.js'; -import {submodule} from '../src/hook.js'; +import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // RE below lint exception: UID2 and EUID are separate modules, but the protocol is the same and shared code makes sense here. // eslint-disable-next-line prebid/validate-imports import { Uid2GetId, Uid2CodeVersion } from './uid2IdSystem_shared.js'; +import {UID2_EIDS} from '../libraries/uid2Eids/uid2Eids.js'; const MODULE_NAME = 'uid2'; const MODULE_REVISION = Uid2CodeVersion; @@ -32,6 +33,19 @@ function createLogger(logger, prefix) { logger(prefix + ' ', ...strings); } } + +function extractIdentityFromParams(params) { + const keysToCheck = ['emailHash', 'phoneHash', 'email', 'phone']; + + for (let key of keysToCheck) { + if (params.hasOwnProperty(key)) { + return { [key]: params[key] }; + } + } + + return {}; +} + const _logInfo = createLogger(logInfo, LOG_PRE_FIX); const _logWarn = createLogger(logWarn, LOG_PRE_FIX); @@ -78,11 +92,20 @@ export const uid2IdSubmodule = { clientId: UID2_CLIENT_ID, internalStorage: ADVERTISING_COOKIE } + + if (FEATURES.UID2_CSTG) { + mappedConfig.cstg = { + serverPublicKey: config?.params?.serverPublicKey, + subscriptionId: config?.params?.subscriptionId, + ...extractIdentityFromParams(config?.params ?? {}) + } + } _logInfo(`UID2 configuration loaded and mapped.`, mappedConfig); const result = Uid2GetId(mappedConfig, storage, _logInfo, _logWarn); _logInfo(`UID2 getId returned`, result); return result; }, + eids: UID2_EIDS }; function decodeImpl(value) { diff --git a/modules/uid2IdSystem.md b/modules/uid2IdSystem.md index a795d9b1aa1..e546f6eafe1 100644 --- a/modules/uid2IdSystem.md +++ b/modules/uid2IdSystem.md @@ -1,11 +1,63 @@ ## UID2 User ID Submodule -UID2 requires initial tokens to be generated server-side. The UID2 module handles storing, providing, and optionally refreshing them. The module can operate in one of two different modes: *Client Refresh* mode or *Server Only* mode. +The UID2 module handles storing, providing, and optionally refreshing tokens. While initial tokens traditionally required server-side generation, the introduction of the *Client-Side Token Generation (CSTG)* mode offers publishers the flexibility to generate UID2 tokens directly from the module, eliminating this need. Publishers can choose to operate the module in one of three distinct modes: *Client Refresh* mode, *Server Only* mode and *Client-Side Token Generation* mode. *Server Only* mode was originally referred to as *legacy mode*, but it is a popular mode for new integrations where publishers prefer to handle token refresh server-side. +*Client-Side Token Generation* mode is included in UID2 module by default. However, it's important to note that this mode is created and made available recently. For publishers who do not intend to use it, you have the option to instruct the build to exclude the code related to this feature: + +``` + $ gulp build --modules=uid2IdSystem --disable UID2_CSTG +``` +If you do plan to use Client-Side Token Generation (CSTG) mode, please consult the UID2 Team first as they will provide required configuration values for you to use (see the Client-Side Token Generation (CSTG) mode section below for details) + **Important information:** UID2 is not designed to be used where GDPR applies. The module checks the passed-in consent data and will not operate if the `gdprApplies` flag is true. +## Client-Side Token Generation (CSTG) mode + +**This mode is created and made available recently. Please consult UID2 Team first as they will provide required configuration values for you to use.** + +For publishers seeking a purely client-side integration without the complexities of server-side involvement, the CSTG mode is highly recommended. This mode requires the provision of a public key, subscription ID and [directly identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii) - either emails or phone numbers. In the CSTG mode, the module takes on the responsibility of encrypting the DII, generating the UID2 token, and handling token refreshes when necessary. + +To configure the module to use this mode, you must: +1. Set `parmas.serverPublicKey` and `params.subscriptionId` (please reach out to the UID2 team to obtain these values) +2. Provide **ONLY ONE DII** by setting **ONLY ONE** of `params.email`/`params.phone`/`params.emailHash`/`params.phoneHash` + +Below is a table that provides guidance on when to use each directly identifying information (DII) parameter, along with information on whether normalization and hashing are required by the publisher for each parameter. + +| DII param | When to use it | Normalization required by publisher? | Hashing required by publisher? | +|------------------|-------------------------------------------------------|--------------------------------------|--------------------------------| +| params.email | When you have users' email address | No | No | +| params.phone | When you have user's phone number | Yes | No | +| params.emailHash | When you have user's hashed, normalized email address | Yes | Yes | +| params.phoneHash | When you have user's hashed, normalized phone number | Yes | Yes | + + +*Note that setting params.email will normalize email addresses, but params.phone requires phone numbers to be normalized.* + +Refer to [Normalization and Encoding](#normalization-and-encoding) for details on email address normalization, SHA-256 hashing and Base64 encoding. + +### CSTG example + +Configuration: +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'uid2', + params: { + serverPublicKey: '...server public key...', + subscriptionId: '...subcription id...', + email: 'user@email.com', + //phone: '+0000000', + //emailHash: '...email hash...', + //phoneHash: '...phone hash ...' + } + }] + } +}); +``` + ## Client Refresh mode This is the recommended mode for most scenarios. In this mode, the full response body from the UID2 Token Generate or Token Refresh endpoint must be provided to the module. As long as the refresh token remains valid, the module will refresh the advertising token as needed. @@ -133,3 +185,80 @@ The below parameters apply only to the UID2 User ID Module integration. | params.uid2Cookie | Optional, Client refresh | String | The name of a cookie which holds the initial UID2 token, set by the server. The cookie should contain JSON in the same format as the uid2Token param. **If uid2Token is supplied, this param is ignored.** | See the sample token above. | | params.uid2ApiBase | Optional, Client refresh | String | Overrides the default UID2 API endpoint. | `"https://prod.uidapi.com"` _(default)_| | params.storage | Optional, Client refresh | String | Specify whether to use `cookie` or `localStorage` for module-internal storage. It is recommended to not provide this and allow the module to use the default. | `localStorage` _(default)_ | +| params.serverPublicKey | Optional, Client-side token generation | String | A public key for encrypting the DII payload for the Operator's CSTG endpoint. **This is required for client-side token generation.** | - | +| params.subscriptionId | Optional, Client-side token generation | String | A publisher Identifier. **This is required for client-side token generation.** | - | +| params.email | Optional, Client-side token generation | String | The user's email address. Provide this parameter if using email as the DII. | `"test@example.com"` | +| params.emailHash | Optional, Client-side token generation | String | A hashed, normalized representation of the user's email. Provide this parameter if using emailHash as the DII. | `"tMmiiTI7IaAcPpQPFQ65uMVCWH8av9jw4cwf/F5HVRQ="` | +| params.phone | Optional, Client-side token generation | String | The user's phone number. Provide this parameter if using phone as the DII. | `"+15555555555"` | +| params.phoneHash | Optional, Client-side token generation | String | A hashed, normalized representation of the user's phone number. Provide this parameter if using phoneHash as the DII. | `"tMmiiTI7IaAcPpQPFQ65uMVCWH8av9jw4cwf/F5HVRQ="` | + +# Normalization and Encoding + +This section provides information about normalizing and encoding [directly Identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii). It's important that, in working with UID2, normalizing and encoding are performed correctly. + +## Introduction +When you're taking user information such as an email address, and following the steps to create a raw UID2 and/or a UID2 advertising token, it's very important that you follow all the required steps. Whether you normalize the information or not, whether you hash it or not, follow the steps exactly. By doing so, you can ensure that the UID2 value you create can be securely and anonymously matched up with other instances of online behavior by the same user. + +>Note: Raw UID2s, and their associated UID2 tokens, are case sensitive. When working with UID2, it's important to pass all IDs and tokens without changing the case. Mismatched IDs can cause ID parsing or token decryption errors. + +## Types of Directly Identifying Information +UID2 supports the following types of directly identifying information (DII): +- Email address +- Phone number + +## Email Address Normalization + +If you send unhashed email addresses to the UID2 Operator Service, the service normalizes the email addresses and then hashes them. If you want to hash the email addresses yourself before sending them, you must normalize them before you hash them. + +> IMPORTANT: Normalizing before hashing ensures that the generated UID2 value will always be the same, so that the data can be matched. If you do not normalize before hashing, this might result in a different UID2, reducing the effectiveness of targeted advertising. + +To normalize an email address, complete the following steps: + +1. Remove leading and trailing spaces. +2. Convert all ASCII characters to lowercase. +3. In `gmail.com` email addresses, remove the following characters from the username part of the email address: + 1. The period (`.` (ASCII code 46)).
For example, normalize `jane.doe@gmail.com` to `janedoe@gmail.com`. + 2. The plus sign (`+` (ASCII code 43)) and all subsequent characters.
For example, normalize `janedoe+home@gmail.com` to `janedoe@gmail.com`. + +## Email Address Hash Encoding + +An email hash is a Base64-encoded SHA-256 hash of a normalized email address. The email address is first normalized, then hashed using the SHA-256 hashing algorithm, and then the resulting bytes of the hash value are encoded using Base64 encoding. Note that the bytes of the hash value are encoded, not the hex-encoded string representation. + +| Type | Example | Comments and Usage | +| :--- | :--- | :--- | +| Normalized email address | `user@example.com` | Normalization is always the first step. | +| SHA-256 hash of normalized email address | `b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514` | This 64-character string is a hex-encoded representation of the 32-byte SHA-256.| +| Hex to Base64 SHA-256 encoding of normalized email address | `tMmiiTI7IaAcPpQPFQ65uMVCWH8av9jw4cwf/F5HVRQ=` | This 44-character string is a Base64-encoded representation of the 32-byte SHA-256.
WARNING: The SHA-256 hash string in the example above is a hex-encoded representation of the hash value. You must Base64-encode the raw bytes of the hash or use a Base64 encoder that takes a hex-encoded value as input.
Use this encoding for `email_hash` values sent in the request body. | + +>WARNING: When applying Base64 encoding, be sure to Base64-encode the raw bytes of the hash or use a Base64 encoder that takes a hex-encoded value as input. + +## Phone Number Normalization + +If you send unhashed phone numbers to the UID2 Operator Service, the service normalizes the phone numbers and then hashes them. If you want to hash the phone numbers yourself before sending them, you must normalize them before you hash them. + +> IMPORTANT: Normalization before hashing ensures that the generated UID2 value will always be the same, so that the data can be matched. If you do not normalize before hashing, this might result in a different UID2, reducing the effectiveness of targeted advertising. + +Here's what you need to know about phone number normalization rules: + +- The UID2 Operator accepts phone numbers in the [E.164](https://en.wikipedia.org/wiki/E.164) format, which is the international phone number format that ensures global uniqueness. +- E.164 phone numbers can have a maximum of 15 digits. +- Normalized E.164 phone numbers use the following syntax, with no spaces, hyphens, parentheses, or other special characters:
+ `[+] [country code] [subscriber number including area code]` + Examples: + - US: `1 (123) 456-7890` is normalized to `+11234567890`. + - Singapore: `65 1243 5678` is normalized to `+6512345678`. + - Sydney, Australia: `(02) 1234 5678` is normalized to drop the leading zero for the city plus include the country code: `+61212345678`. + +## Phone Number Hash Encoding + +A phone number hash is a Base64-encoded SHA-256 hash of a normalized phone number. The phone number is first normalized, then hashed using the SHA-256 hashing algorithm, and the resulting hex value is encoded using Base64 encoding. + +The example below shows a simple input phone number, and the result as each step is applied to arrive at a secure, opaque, URL-safe value. + +| Type | Example | Comments and Usage | +| :--- | :--- | :--- | +| Normalized phone number | `+12345678901` | Normalization is always the first step. | +| SHA-256 hash of normalized phone number | `10e6f0b47054a83359477dcb35231db6de5c69fb1816e1a6b98e192de9e5b9ee` |This 64-character string is a hex-encoded representation of the 32-byte SHA-256. | +| Hex to Base64 SHA-256 encoding of normalized and hashed phone number | `EObwtHBUqDNZR33LNSMdtt5cafsYFuGmuY4ZLenlue4=` | This 44-character string is a Base64-encoded representation of the 32-byte SHA-256.
NOTE: The SHA-256 hash is a hexadecimal value. You must use a Base64 encoder that takes a hex value as input. Use this encoding for `phone_hash` values sent in the request body. | + +>WARNING: When applying Base64 encoding, be sure to use a function that takes a hex value as input. If you use a function that takes text as input, the result is a longer string which is invalid for the purposes of UID2. diff --git a/modules/uid2IdSystem_shared.js b/modules/uid2IdSystem_shared.js index 0f6894a9d3e..29837c5f012 100644 --- a/modules/uid2IdSystem_shared.js +++ b/modules/uid2IdSystem_shared.js @@ -1,4 +1,7 @@ /* eslint-disable no-console */ +import { ajax } from '../src/ajax.js'; +import { cyrb53Hash } from '../src/utils.js'; + export const Uid2CodeVersion = '1.1'; function isValidIdentity(identity) { @@ -13,6 +16,7 @@ export class Uid2ApiClient { this._logInfo = logInfo; this._logWarn = logWarn; } + createArrayBuffer(text) { const arrayBuffer = new Uint8Array(text.length); for (let i = 0; i < text.length; i++) { @@ -36,49 +40,58 @@ export class Uid2ApiClient { } callRefreshApi(refreshDetails) { const url = this._baseUrl + '/v2/token/refresh'; - const req = new XMLHttpRequest(); - req.overrideMimeType('text/plain'); - req.open('POST', url, true); - req.setRequestHeader('X-UID2-Client-Version', this._clientVersion); let resolvePromise; let rejectPromise; const promise = new Promise((resolve, reject) => { resolvePromise = resolve; rejectPromise = reject; }); - req.onreadystatechange = () => { - if (req.readyState !== req.DONE) { return; } - try { - if (!refreshDetails.refresh_response_key || req.status !== 200) { - this._logInfo('Error status OR no response decryption key available, assuming unencrypted JSON'); - const response = JSON.parse(req.responseText); + this._logInfo('Sending refresh request', refreshDetails); + ajax(url, { + success: (responseText) => { + try { + if (!refreshDetails.refresh_response_key) { + this._logInfo('No response decryption key available, assuming unencrypted JSON'); + const response = JSON.parse(responseText); + const result = this.ResponseToRefreshResult(response); + if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } + } else { + this._logInfo('Decrypting refresh API response'); + const encodeResp = this.createArrayBuffer(atob(responseText)); + window.crypto.subtle.importKey('raw', this.createArrayBuffer(atob(refreshDetails.refresh_response_key)), { name: 'AES-GCM' }, false, ['decrypt']).then((key) => { + this._logInfo('Imported decryption key') + // returns the symmetric key + window.crypto.subtle.decrypt({ + name: 'AES-GCM', + iv: encodeResp.slice(0, 12), + tagLength: 128, // The tagLength you used to encrypt (if any) + }, key, encodeResp.slice(12)).then((decrypted) => { + const decryptedResponse = String.fromCharCode(...new Uint8Array(decrypted)); + this._logInfo('Decrypted to:', decryptedResponse); + const response = JSON.parse(decryptedResponse); + const result = this.ResponseToRefreshResult(response); + if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } + }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); + }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); + } + } catch (_err) { + rejectPromise(responseText); + } + }, + error: (error, xhr) => { + try { + this._logInfo('Error status, assuming unencrypted JSON'); + const response = JSON.parse(xhr.responseText); const result = this.ResponseToRefreshResult(response); if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } - } else { - this._logInfo('Decrypting refresh API response'); - const encodeResp = this.createArrayBuffer(atob(req.responseText)); - window.crypto.subtle.importKey('raw', this.createArrayBuffer(atob(refreshDetails.refresh_response_key)), { name: 'AES-GCM' }, false, ['decrypt']).then((key) => { - this._logInfo('Imported decryption key') - // returns the symmetric key - window.crypto.subtle.decrypt({ - name: 'AES-GCM', - iv: encodeResp.slice(0, 12), - tagLength: 128, // The tagLength you used to encrypt (if any) - }, key, encodeResp.slice(12)).then((decrypted) => { - const decryptedResponse = String.fromCharCode(...new Uint8Array(decrypted)); - this._logInfo('Decrypted to:', decryptedResponse); - const response = JSON.parse(decryptedResponse); - const result = this.ResponseToRefreshResult(response); - if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } - }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); - }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); + } catch (_e) { + rejectPromise(error) } - } catch (err) { - rejectPromise(err); } - }; - this._logInfo('Sending refresh request', refreshDetails); - req.send(refreshDetails.refresh_token); + }, refreshDetails.refresh_token, { method: 'POST', + customHeaders: { + 'X-UID2-Client-Version': this._clientVersion + } }); return promise; } } @@ -160,18 +173,507 @@ function refreshTokenAndStore(baseUrl, token, clientId, storageManager, _logInfo originalToken: token, latestToken: response.identity, }; + let storedTokens = storageManager.getStoredValueWithFallback(); + if (storedTokens?.originalIdentity) tokens.originalIdentity = storedTokens.originalIdentity; storageManager.storeValue(tokens); return tokens; }); } +let clientSideTokenGenerator; +if (FEATURES.UID2_CSTG) { + const SERVER_PUBLIC_KEY_PREFIX_LENGTH = 9; + + clientSideTokenGenerator = { + isCSTGOptionsValid(maybeOpts, _logWarn) { + if (typeof maybeOpts !== 'object' || maybeOpts === null) { + _logWarn('CSTG opts must be an object'); + return false; + } + + const opts = maybeOpts; + if (typeof opts.serverPublicKey !== 'string') { + _logWarn('CSTG opts.serverPublicKey must be a string'); + return false; + } + const serverPublicKeyPrefix = /^UID2-X-[A-Z]-.+/; + if (!serverPublicKeyPrefix.test(opts.serverPublicKey)) { + _logWarn( + `CSTG opts.serverPublicKey must match the regular expression ${serverPublicKeyPrefix}` + ); + return false; + } + // We don't do any further validation of the public key, as we will find out + // later if it's valid by using importKey. + + if (typeof opts.subscriptionId !== 'string') { + _logWarn('CSTG opts.subscriptionId must be a string'); + return false; + } + if (opts.subscriptionId.length === 0) { + _logWarn('CSTG opts.subscriptionId is empty'); + return false; + } + return true; + }, + + getValidIdentity(opts, _logWarn) { + if (opts.emailHash) { + if (!UID2DiiNormalization.isBase64Hash(opts.emailHash)) { + _logWarn('CSTG opts.emailHash is invalid'); + return; + } + return { email_hash: opts.emailHash }; + } + + if (opts.phoneHash) { + if (!UID2DiiNormalization.isBase64Hash(opts.phoneHash)) { + _logWarn('CSTG opts.phoneHash is invalid'); + return; + } + return { phone_hash: opts.phoneHash }; + } + + if (opts.email) { + const normalizedEmail = UID2DiiNormalization.normalizeEmail(opts.email); + if (normalizedEmail === undefined) { + _logWarn('CSTG opts.email is invalid'); + return; + } + return { email: normalizedEmail }; + } + + if (opts.phone) { + if (!UID2DiiNormalization.isNormalizedPhone(opts.phone)) { + _logWarn('CSTG opts.phone is invalid'); + return; + } + return { phone: opts.phone }; + } + }, + + isStoredTokenInvalid(cstgIdentity, storedTokens, _logInfo, _logWarn) { + if (storedTokens) { + const identity = Object.values(cstgIdentity)[0]; + if (!this.isStoredTokenFromSameIdentity(storedTokens, identity)) { + _logInfo( + 'CSTG supplied new identity - ignoring stored value.', + storedTokens.originalIdentity, + cstgIdentity + ); + // Stored token wasn't originally sourced from the provided identity - ignore the stored value. A new user has logged in? + return true; + } + } + return false; + }, + + async generateTokenAndStore( + baseUrl, + cstgOpts, + cstgIdentity, + storageManager, + _logInfo, + _logWarn + ) { + _logInfo('UID2 cstg opts provided: ', JSON.stringify(cstgOpts)); + const client = new UID2CstgApiClient( + { baseUrl, cstg: cstgOpts }, + _logInfo, + _logWarn + ); + const response = await client.generateToken(cstgIdentity); + _logInfo('CSTG endpoint responded with:', response); + const tokens = { + originalIdentity: this.encodeOriginalIdentity(cstgIdentity), + latestToken: response.identity, + }; + storageManager.storeValue(tokens); + return tokens; + }, + + isStoredTokenFromSameIdentity(storedTokens, identity) { + if (!storedTokens.originalIdentity) return false; + return ( + cyrb53Hash(identity, storedTokens.originalIdentity.salt) === + storedTokens.originalIdentity.identity + ); + }, + + encodeOriginalIdentity(identity) { + const identityValue = Object.values(identity)[0]; + const salt = Math.floor(Math.random() * Math.pow(2, 32)); + return { + identity: cyrb53Hash(identityValue, salt), + salt, + }; + }, + }; + + class UID2DiiNormalization { + static EMAIL_EXTENSION_SYMBOL = '+'; + static EMAIL_DOT = '.'; + static GMAIL_DOMAIN = 'gmail.com'; + + static isBase64Hash(value) { + if (!(value && value.length === 44)) { + return false; + } + + try { + return btoa(atob(value)) === value; + } catch (err) { + return false; + } + } + + static isNormalizedPhone(phone) { + return /^\+[0-9]{10,15}$/.test(phone); + } + + static normalizeEmail(email) { + if (!email || !email.length) return; + + const parsedEmail = email.trim().toLowerCase(); + if (parsedEmail.indexOf(' ') > 0) return; + + const emailParts = this.splitEmailIntoAddressAndDomain(parsedEmail); + if (!emailParts) return; + + const { address, domain } = emailParts; + + const emailIsGmail = this.isGmail(domain); + const parsedAddress = this.normalizeAddressPart( + address, + emailIsGmail, + emailIsGmail + ); + + return parsedAddress ? `${parsedAddress}@${domain}` : undefined; + } + + static splitEmailIntoAddressAndDomain(email) { + const parts = email.split('@'); + if ( + parts.length !== 2 || + parts.some((part) => part === '') + ) { return; } + + return { + address: parts[0], + domain: parts[1], + }; + } + + static isGmail(domain) { + return domain === this.GMAIL_DOMAIN; + } + + static dropExtension(address, extensionSymbol = this.EMAIL_EXTENSION_SYMBOL) { + return address.split(extensionSymbol)[0]; + } + + static normalizeAddressPart(address, shouldRemoveDot, shouldDropExtension) { + let parsedAddress = address; + if (shouldRemoveDot) { parsedAddress = parsedAddress.replaceAll(this.EMAIL_DOT, ''); } + if (shouldDropExtension) parsedAddress = this.dropExtension(parsedAddress); + return parsedAddress; + } + } + + class UID2CstgApiClient { + constructor(opts, logInfo, logWarn) { + this._baseUrl = opts.baseUrl; + this._serverPublicKey = opts.cstg.serverPublicKey; + this._subscriptionId = opts.cstg.subscriptionId; + this._logInfo = logInfo; + this._logWarn = logWarn; + } + + hasStatusResponse(response) { + return typeof response === 'object' && response && response.status; + } + + isCstgApiSuccessResponse(response) { + return ( + this.hasStatusResponse(response) && + response.status === 'success' && + isValidIdentity(response.body) + ); + } + + isCstgApiClientErrorResponse(response) { + return ( + this.hasStatusResponse(response) && + response.status === 'client_error' && + typeof response.message === 'string' + ); + } + + isCstgApiForbiddenResponse(response) { + return ( + this.hasStatusResponse(response) && + response.status === 'invalid_http_origin' && + typeof response.message === 'string' + ); + } + + stripPublicKeyPrefix(serverPublicKey) { + return serverPublicKey.substring(SERVER_PUBLIC_KEY_PREFIX_LENGTH); + } + + async generateCstgRequest(cstgIdentity) { + if ('email_hash' in cstgIdentity || 'phone_hash' in cstgIdentity) { + return cstgIdentity; + } + if ('email' in cstgIdentity) { + const emailHash = await UID2CstgCrypto.hash(cstgIdentity.email); + return { email_hash: emailHash }; + } + if ('phone' in cstgIdentity) { + const phoneHash = await UID2CstgCrypto.hash(cstgIdentity.phone); + return { phone_hash: phoneHash }; + } + } + + async generateToken(cstgIdentity) { + const request = await this.generateCstgRequest(cstgIdentity); + this._logInfo('Building CSTG request for', request); + const box = await UID2CstgBox.build( + this.stripPublicKeyPrefix(this._serverPublicKey) + ); + const encoder = new TextEncoder(); + const now = Date.now(); + const { iv, ciphertext } = await box.encrypt( + encoder.encode(JSON.stringify(request)), + encoder.encode(JSON.stringify([now])) + ); + + const exportedPublicKey = await UID2CstgCrypto.exportPublicKey( + box.clientPublicKey + ); + const requestBody = { + payload: UID2CstgCrypto.bytesToBase64(new Uint8Array(ciphertext)), + iv: UID2CstgCrypto.bytesToBase64(new Uint8Array(iv)), + public_key: UID2CstgCrypto.bytesToBase64( + new Uint8Array(exportedPublicKey) + ), + timestamp: now, + subscription_id: this._subscriptionId, + }; + return this.callCstgApi(requestBody, box); + } + + async callCstgApi(requestBody, box) { + const url = this._baseUrl + '/v2/token/client-generate'; + let resolvePromise; + let rejectPromise; + const promise = new Promise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + + this._logInfo('Sending CSTG request', requestBody); + ajax( + url, + { + success: async (responseText, xhr) => { + try { + const encodedResp = UID2CstgCrypto.base64ToBytes(responseText); + const decrypted = await box.decrypt( + encodedResp.slice(0, 12), + encodedResp.slice(12) + ); + const decryptedResponse = new TextDecoder().decode(decrypted); + const response = JSON.parse(decryptedResponse); + if (this.isCstgApiSuccessResponse(response)) { + resolvePromise({ + status: 'success', + identity: response.body, + }); + } else { + // A 200 should always be a success response. + // Something has gone wrong. + rejectPromise( + `API error: Response body was invalid for HTTP status 200: ${decryptedResponse}` + ); + } + } catch (err) { + rejectPromise(err); + } + }, + error: (error, xhr) => { + try { + if (xhr.status === 400) { + const response = JSON.parse(xhr.responseText); + if (this.isCstgApiClientErrorResponse(response)) { + rejectPromise(`Client error: ${response.message}`); + } else { + // A 400 should always be a client error. + // Something has gone wrong. + rejectPromise( + `API error: Response body was invalid for HTTP status 400: ${xhr.responseText}` + ); + } + } else if (xhr.status === 403) { + const response = JSON.parse(xhr.responseText); + if (this.isCstgApiForbiddenResponse(xhr)) { + rejectPromise(`Forbidden: ${response.message}`); + } else { + // A 403 should always be a forbidden response. + // Something has gone wrong. + rejectPromise( + `API error: Response body was invalid for HTTP status 403: ${xhr.responseText}` + ); + } + } else { + rejectPromise( + `API error: Unexpected HTTP status ${xhr.status}: ${error}` + ); + } + } catch (_e) { + rejectPromise(error); + } + }, + }, + JSON.stringify(requestBody), + { method: 'POST' } + ); + return promise; + } + } + + class UID2CstgBox { + static _namedCurve = 'P-256'; + constructor(clientPublicKey, sharedKey) { + this._clientPublicKey = clientPublicKey; + this._sharedKey = sharedKey; + } + + static async build(serverPublicKey) { + const clientKeyPair = await UID2CstgCrypto.generateKeyPair( + UID2CstgBox._namedCurve + ); + const importedServerPublicKey = await UID2CstgCrypto.importPublicKey( + serverPublicKey, + this._namedCurve + ); + const sharedKey = await UID2CstgCrypto.deriveKey( + importedServerPublicKey, + clientKeyPair.privateKey + ); + return new UID2CstgBox(clientKeyPair.publicKey, sharedKey); + } + + async encrypt(plaintext, additionalData) { + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = await window.crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv, + additionalData, + }, + this._sharedKey, + plaintext + ); + return { iv, ciphertext }; + } + + async decrypt(iv, ciphertext) { + return window.crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv, + }, + this._sharedKey, + ciphertext + ); + } + + get clientPublicKey() { + return this._clientPublicKey; + } + } + + class UID2CstgCrypto { + static base64ToBytes(base64) { + const binString = atob(base64); + return Uint8Array.from(binString, (m) => m.codePointAt(0)); + } + + static bytesToBase64(bytes) { + const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join( + '' + ); + return btoa(binString); + } + + static async generateKeyPair(namedCurve) { + const params = { + name: 'ECDH', + namedCurve: namedCurve, + }; + return window.crypto.subtle.generateKey(params, false, ['deriveKey']); + } + + static async importPublicKey(publicKey, namedCurve) { + const params = { + name: 'ECDH', + namedCurve: namedCurve, + }; + return window.crypto.subtle.importKey( + 'spki', + this.base64ToBytes(publicKey), + params, + false, + [] + ); + } + + static exportPublicKey(publicKey) { + return window.crypto.subtle.exportKey('spki', publicKey); + } + + static async deriveKey(serverPublicKey, clientPrivateKey) { + return window.crypto.subtle.deriveKey( + { + name: 'ECDH', + public: serverPublicKey, + }, + clientPrivateKey, + { + name: 'AES-GCM', + length: 256, + }, + false, + ['encrypt', 'decrypt'] + ); + } + + static async hash(value) { + const hash = await window.crypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(value) + ); + return this.bytesToBase64(new Uint8Array(hash)); + } + } +} + export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { let suppliedToken = null; const preferLocalStorage = (config.storage !== 'cookie'); const storageManager = new Uid2StorageManager(prebidStorageManager, preferLocalStorage, config.internalStorage, _logInfo); _logInfo(`Module is using ${preferLocalStorage ? 'local storage' : 'cookies'} for internal storage.`); - if (config.paramToken) { + const isCstgEnabled = + clientSideTokenGenerator && + clientSideTokenGenerator.isCSTGOptionsValid(config.cstg, _logWarn); + if (isCstgEnabled) { + _logInfo(`Module is using client-side token generation.`); + // Ignores config.paramToken and config.serverCookieName if any is provided + suppliedToken = null; + } else if (config.paramToken) { suppliedToken = config.paramToken; _logInfo('Read token from params', suppliedToken); } else if (config.serverCookieName) { @@ -184,7 +686,7 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { if (storedTokens && typeof storedTokens === 'string') { // Stored value is a plain token - if no token is supplied, just use the stored value. - if (!suppliedToken) { + if (!suppliedToken && !isCstgEnabled) { _logInfo('Returning legacy cookie value.'); return { id: storedTokens }; } @@ -200,11 +702,31 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { storedTokens = null; } } - // At this point, any legacy values or superseded stored tokens have been nulled out. + + if (FEATURES.UID2_CSTG && isCstgEnabled) { + const cstgIdentity = clientSideTokenGenerator.getValidIdentity(config.cstg, _logWarn); + if (cstgIdentity) { + if (storedTokens && clientSideTokenGenerator.isStoredTokenInvalid(cstgIdentity, storedTokens, _logInfo, _logWarn)) { + storedTokens = null; + } + + if (!storedTokens || Date.now() > storedTokens.latestToken.refresh_expires) { + const promise = clientSideTokenGenerator.generateTokenAndStore(config.apiBaseUrl, config.cstg, cstgIdentity, storageManager, _logInfo, _logWarn); + _logInfo('Generate token using CSTG'); + return { callback: (cb) => { + promise.then((result) => { + _logInfo('Token generation responded, passing the new token on.', result); + cb(result); + }); + } }; + } + } + } + const useSuppliedToken = !(storedTokens?.latestToken) || (suppliedToken && suppliedToken.identity_expires > storedTokens.latestToken.identity_expires); const newestAvailableToken = useSuppliedToken ? suppliedToken : storedTokens.latestToken; _logInfo('UID2 module selected latest token', useSuppliedToken, newestAvailableToken); - if (!newestAvailableToken || Date.now() > newestAvailableToken.refresh_expires) { + if ((!newestAvailableToken || Date.now() > newestAvailableToken.refresh_expires)) { _logInfo('Newest available token is expired and not refreshable.'); return { id: null }; } @@ -227,6 +749,9 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { originalToken: suppliedToken ?? storedTokens?.originalToken, latestToken: newestAvailableToken, }; + if (FEATURES.UID2_CSTG && isCstgEnabled) { + tokens.originalIdentity = storedTokens?.originalIdentity; + } storageManager.storeValue(tokens); return { id: tokens }; } diff --git a/modules/underdogmediaBidAdapter.js b/modules/underdogmediaBidAdapter.js index 7561cdb60de..54b74c7ccd4 100644 --- a/modules/underdogmediaBidAdapter.js +++ b/modules/underdogmediaBidAdapter.js @@ -4,7 +4,6 @@ import { getWindowSelf, getWindowTop, isGptPubadsDefined, - isSlotMatchingAdUnitCode, logInfo, logMessage, logWarn, @@ -12,6 +11,7 @@ import { } from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'underdogmedia'; const UDM_ADAPTER_VERSION = '7.30V'; @@ -71,7 +71,7 @@ export const spec = { let data = { dt: 10, gdpr: {}, - pbTimeout: config.getConfig('bidderTimeout'), + pbTimeout: +config.getConfig('bidderTimeout') || 3001, // KP: convert to number and if NaN we default to 3001. Particular value to let us know that there was a problem in converting pbTimeout pbjsVersion: prebidVersion, placements: [], ref: deepAccess(bidderRequest, 'refererInfo.page') ? bidderRequest.refererInfo.page : undefined, diff --git a/modules/undertoneBidAdapter.js b/modules/undertoneBidAdapter.js index 9dcbefd374b..c7e8102ffc9 100644 --- a/modules/undertoneBidAdapter.js +++ b/modules/undertoneBidAdapter.js @@ -139,6 +139,7 @@ export const spec = { domain: domain, placementId: bidReq.params.placementId != undefined ? bidReq.params.placementId : null, publisherId: bidReq.params.publisherId, + gpid: deepAccess(bidReq, 'ortb2Imp.ext.gpid', deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot', '')), sizes: bidReq.sizes, params: bidReq.params }; diff --git a/modules/undertoneBidAdapter.md b/modules/undertoneBidAdapter.md index 8e0b234fd7a..1cfc912e360 100644 --- a/modules/undertoneBidAdapter.md +++ b/modules/undertoneBidAdapter.md @@ -23,7 +23,7 @@ Module that connects to Undertone's demand sources { bidder: "undertone", params: { - placementId: '10433394', + placementId: 1234, publisherId: 12345 } } diff --git a/modules/unifiedIdSystem.js b/modules/unifiedIdSystem.js index 8ec5fcd3f90..5576c40d960 100644 --- a/modules/unifiedIdSystem.js +++ b/modules/unifiedIdSystem.js @@ -67,6 +67,17 @@ export const unifiedIdSubmodule = { ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true}); }; return {callback: resp}; + }, + eids: { + 'tdid': { + source: 'adserver.org', + atype: 1, + getUidExt: function() { + return { + rtiPartner: 'TDID' + }; + } + }, } }; diff --git a/modules/userId/eids.js b/modules/userId/eids.js index e8e83ca19a0..aad570f20df 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -1,421 +1,10 @@ -import { pick, isFn, isStr, isPlainObject, deepAccess } from '../../src/utils.js'; +import {deepAccess, isFn, isPlainObject, isStr} from '../../src/utils.js'; -// Each user-id sub-module is expected to mention respective config here -export const USER_IDS_CONFIG = { - - // key-name : {config} - - // GrowthCode - 'growthCodeId': { - getValue: function(data) { - return data.gc_id - }, - source: 'growthcode.io', - atype: 1, - getUidExt: function(data) { - const extendedData = pick(data, [ - 'h1', - 'h2', - 'h3', - ]); - if (Object.keys(extendedData).length) { - return extendedData; - } - } - }, - - // utiq - 'utiq': { - source: 'utiq.com', - atype: 1, - getValue: function (data) { - return data; - }, - }, - - // intentIqId - 'intentIqId': { - source: 'intentiq.com', - atype: 1 - }, - - // naveggId - 'naveggId': { - source: 'navegg.com', - atype: 1 - }, - - // pairId - 'pairId': { - source: 'google.com', - atype: 571187 - }, - - // justId - 'justId': { - source: 'justtag.com', - atype: 1 - }, - - // pubCommonId - 'pubcid': { - source: 'pubcid.org', - atype: 1 - }, - - // unifiedId - 'tdid': { - source: 'adserver.org', - atype: 1, - getUidExt: function() { - return { - rtiPartner: 'TDID' - }; - } - }, - - // id5Id - 'id5id': { - getValue: function(data) { - return data.uid - }, - source: 'id5-sync.com', - atype: 1, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - - // ftrack - 'ftrackId': { - source: 'flashtalking.com', - atype: 1, - getValue: function(data) { - let value = ''; - if (data && data.ext && data.ext.DeviceID) { - value = data.ext.DeviceID; - } - return value; - }, - getUidExt: function(data) { - return data && data.ext; - } - }, - - // parrableId - 'parrableId': { - source: 'parrable.com', - atype: 1, - getValue: function(parrableId) { - if (parrableId.eid) { - return parrableId.eid; - } - if (parrableId.ccpaOptout) { - // If the EID was suppressed due to a non consenting ccpa optout then - // we still wish to provide this as a reason to the adapters - return ''; - } - return null; - }, - getUidExt: function(parrableId) { - const extendedData = pick(parrableId, [ - 'ibaOptout', - 'ccpaOptout' - ]); - if (Object.keys(extendedData).length) { - return extendedData; - } - } - }, - - // identityLink - 'idl_env': { - source: 'liveramp.com', - atype: 3 - }, - - // liveIntentId - 'lipb': { - getValue: function(data) { - return data.lipbid; - }, - source: 'liveintent.com', - atype: 3, - getEidExt: function(data) { - if (Array.isArray(data.segments) && data.segments.length) { - return { - segments: data.segments - }; - } - } - }, - - // bidswitchId - 'bidswitch': { - source: 'bidswitch.net', - atype: 3, - getValue: function(data) { - return data.id; - } - }, - - // medianetId - 'medianet': { - source: 'media.net', - atype: 3, - getValue: function(data) { - return data.id; - } - }, - - // britepoolId - 'britepoolid': { - source: 'britepool.com', - atype: 3 - }, - - // dmdId - 'dmdId': { - source: 'hcn.health', - atype: 3 - }, - - // lotamePanoramaId - lotamePanoramaId: { - source: 'crwdcntrl.net', - atype: 1, - }, - - // criteo - 'criteoId': { - source: 'criteo.com', - atype: 1 - }, - - // merkleId - 'merkleId': { - atype: 3, - getSource: function(data) { - if (data?.ext?.ssp) { - return `${data.ext.ssp}.merkleinc.com` - } - return 'merkleinc.com' - }, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.keyID) { - return { - keyID: data.keyID - } - } - if (data.ext) { - return data.ext; - } - } - }, - - // NetId - 'netId': { - source: 'netid.de', - atype: 1 - }, - - // zeotapIdPlus - 'IDP': { - source: 'zeotap.com', - atype: 1 - }, - - // hadronId - 'hadronId': { - source: 'audigent.com', - atype: 1 - }, - - // quantcastId - 'quantcastId': { - source: 'quantcast.com', - atype: 1 - }, - - // IDx - 'idx': { - source: 'idx.lat', - atype: 1 - }, - - // Verizon Media ConnectID - 'connectid': { - source: 'verizonmedia.com', - atype: 3 - }, - - // Neustar Fabrick - 'fabrickId': { - source: 'neustar.biz', - atype: 1 - }, - - // MediaWallah OpenLink - 'mwOpenLinkId': { - source: 'mediawallahscript.com', - atype: 1 - }, - - 'tapadId': { - source: 'tapad.com', - atype: 1 - }, - - // Novatiq Snowflake - 'novatiq': { - getValue: function(data) { - if (data.snowflake.id === undefined) { - return data.snowflake; - } - - return data.snowflake.id; - }, - source: 'novatiq.com', - }, - - 'uid2': { - source: 'uidapi.com', - atype: 3, - getValue: function(data) { - return data.id; - } - }, - - 'euid': { - source: 'euid.eu', - atype: 3, - getValue: function(data) { - return data.id; - } - }, - - 'deepintentId': { - source: 'deepintent.com', - atype: 3 - }, - - // Admixer Id - 'admixerId': { - source: 'admixer.net', - atype: 3 - }, - - // Adtelligent Id - 'adtelligentId': { - source: 'adtelligent.com', - atype: 3 - }, - - amxId: { - source: 'amxdt.net', - atype: 1, - }, - - 'publinkId': { - source: 'epsilon.com', - atype: 3 - }, - - 'kpuid': { - source: 'kpuid.com', - atype: 3 - }, - - 'imppid': { - source: 'ppid.intimatemerger.com', - atype: 1 - }, - - 'imuid': { - source: 'intimatemerger.com', - atype: 1 - }, - - // Yahoo ConnectID - 'connectId': { - source: 'yahoo.com', - atype: 3 - }, - - // Adquery ID - 'qid': { - source: 'adquery.io', - atype: 1 - }, - - // DAC ID - 'dacId': { - source: 'impact-ad.jp', - atype: 1 - }, - - // 33across ID - '33acrossId': { - source: '33across.com', - atype: 1, - getValue: function(data) { - return data.envelope; - } - }, - - // tncId - 'tncid': { - source: 'thenewco.it', - atype: 3 - }, - - // Gravito MP ID - 'gravitompId': { - source: 'gravito.net', - atype: 1 - }, - - // czechAdId - 'czechAdId': { - source: 'czechadid.cz', - atype: 1 - }, - - // OneKey Data - 'oneKeyData': { - getValue: function(data) { - if (data && Array.isArray(data.identifiers) && data.identifiers[0]) { - return data.identifiers[0].value; - } - }, - source: 'paf', - atype: 1, - getEidExt: function(data) { - if (data && data.preferences) { - return {preferences: data.preferences}; - } - }, - getUidExt: function(data) { - if (data && Array.isArray(data.identifiers) && data.identifiers[0]) { - const id = data.identifiers[0]; - return { - version: id.version, - type: id.type, - source: id.source - }; - } - } - } -}; +export const EID_CONFIG = new Map(); // this function will create an eid object for the given UserId sub-module function createEidObject(userIdData, subModuleKey) { - const conf = USER_IDS_CONFIG[subModuleKey]; + const conf = EID_CONFIG.get(subModuleKey); if (conf && userIdData) { let eid = {}; eid.source = isFn(conf['getSource']) ? conf['getSource'](userIdData) : conf['source']; @@ -443,34 +32,23 @@ function createEidObject(userIdData, subModuleKey) { return null; } -// this function will generate eids array for all available IDs in bidRequest.userId -// this function will be called by userId module -// if any adapter does not want any particular userId to be passed then adapter can use Array.filter(e => e.source != 'tdid') export function createEidsArray(bidRequestUserId) { - let eids = []; - - for (const subModuleKey in bidRequestUserId) { - if (bidRequestUserId.hasOwnProperty(subModuleKey)) { - if (subModuleKey === 'pubProvidedId') { - eids = eids.concat(bidRequestUserId['pubProvidedId']); - } else if (Array.isArray(bidRequestUserId[subModuleKey])) { - bidRequestUserId[subModuleKey].forEach((config, index, arr) => { - const eid = createEidObject(config, subModuleKey); - - if (eid) { - eids.push(eid); - } - }) - } else { - const eid = createEidObject(bidRequestUserId[subModuleKey], subModuleKey); - if (eid) { - eids.push(eid); - } - } + const allEids = {}; + function collect(eid) { + const key = JSON.stringify([eid.source?.toLowerCase(), eid.ext]); + if (allEids.hasOwnProperty(key)) { + allEids[key].uids.push(...eid.uids); + } else { + allEids[key] = eid; } } - return eids; + Object.entries(bidRequestUserId).forEach(([name, values]) => { + values = Array.isArray(values) ? values : [values]; + const eids = name === 'pubProvidedId' ? values : values.map(value => createEidObject(value, name)); + eids.filter(eid => eid != null).forEach(collect); + }) + return Object.values(allEids); } /** @@ -481,11 +59,12 @@ export function buildEidPermissions(submodules) { submodules.filter(i => isPlainObject(i.idObj) && Object.keys(i.idObj).length) .forEach(i => { Object.keys(i.idObj).forEach(key => { + const eidConf = EID_CONFIG.get(key) || {}; if (deepAccess(i, 'config.bidders') && Array.isArray(i.config.bidders) && - deepAccess(USER_IDS_CONFIG, key + '.source')) { + eidConf.source) { eidPermissions.push( { - source: USER_IDS_CONFIG[key].source, + source: eidConf.source, bidders: i.config.bidders } ); diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 90052cd6301..04073923ed1 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -110,7 +110,10 @@ userIdAsEids = [ source: 'bidswitch.net', uids: [{ id: 'some-random-id-value', - atype: 3 + atype: 3, + ext: { + provider: 'liveintent.com' + } }] }, @@ -118,7 +121,21 @@ userIdAsEids = [ source: 'media.net', uids: [{ id: 'some-random-id-value', - atype: 3 + atype: 3, + ext: { + provider: 'liveintent.com' + } + }] + }, + + { + source: 'rubiconproject.com', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { + provider: 'liveintent.com' + } }] }, @@ -255,6 +272,13 @@ userIdAsEids = [ id: 'some-random-id-value', atype: 3 }] + }, + { + source: 'euid.eu', + uids: [{ + id: 'some-random-id-value', + atype: 3 + }] } ] ``` diff --git a/modules/userId/index.js b/modules/userId/index.js index 91654981c22..b20b38f0e40 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -133,7 +133,7 @@ import {getGlobal} from '../../src/prebidGlobal.js'; import adapterManager, {gdprDataHandler} from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; import {module, ready as hooksReady} from '../../src/hook.js'; -import {buildEidPermissions, createEidsArray, USER_IDS_CONFIG} from './eids.js'; +import {buildEidPermissions, createEidsArray, EID_CONFIG} from './eids.js'; import { getCoreStorageManager, getStorageManager, @@ -141,7 +141,6 @@ import { STORAGE_TYPE_LOCALSTORAGE } from '../../src/storageManager.js'; import { - cyrb53Hash, deepAccess, deepSetValue, delayExecution, @@ -162,7 +161,7 @@ import {defer, GreedyPromise} from '../../src/utils/promise.js'; import {registerOrtbProcessor, REQUEST} from '../../src/pbjsORTB.js'; import {newMetrics, timedAuctionHook, useMetrics} from '../../src/utils/perfMetrics.js'; import {findRootDomain} from '../../src/fpd/rootDomain.js'; -import {GDPR_GVLIDS} from '../../src/consentHandler.js'; +import {allConsent, GDPR_GVLIDS} from '../../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../../src/activities/modules.js'; import {isActivityAllowed} from '../../src/activities/rules.js'; import {ACTIVITY_ENRICH_EIDS} from '../../src/activities/activities.js'; @@ -173,10 +172,6 @@ const COOKIE = STORAGE_TYPE_COOKIES; const LOCAL_STORAGE = STORAGE_TYPE_LOCALSTORAGE; const DEFAULT_SYNC_DELAY = 500; const NO_AUCTION_DELAY = 0; -const CONSENT_DATA_COOKIE_STORAGE_CONFIG = { - name: '_pbjs_userid_consent_data', - expires: 30 // 30 days expiration, which should match how often consent is refreshed by CMPs -}; export const PBJS_USER_ID_OPTOUT_NAME = '_pbjs_id_optout'; export const coreStorage = getCoreStorageManager('userId'); export const dep = { @@ -195,6 +190,9 @@ let initializedSubmodules; /** @type {SubmoduleConfig[]} */ let configRegistry = []; +/** @type {Object} */ +let idPriority = {}; + /** @type {Submodule[]} */ let submoduleRegistry = []; @@ -229,6 +227,7 @@ function submoduleMetrics(moduleName) { /** @param {Submodule[]} submodules */ export function setSubmoduleRegistry(submodules) { submoduleRegistry = submodules; + updateEIDConfig(submodules); } function cookieSetter(submodule, storageMgr) { @@ -257,11 +256,13 @@ export function setStoredValue(submodule, value) { if (storage.type === COOKIE) { const setCookie = cookieSetter(submodule); setCookie(null, valueStr, expiresStr); + setCookie('_cst', getConsentHash(), expiresStr); if (typeof storage.refreshInSeconds === 'number') { setCookie('_last', new Date().toUTCString(), expiresStr); } } else if (storage.type === LOCAL_STORAGE) { mgr.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); + mgr.setDataInLocalStorage(`${storage.name}_cst`, getConsentHash()); mgr.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); if (typeof storage.refreshInSeconds === 'number') { mgr.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); @@ -279,11 +280,11 @@ export function deleteStoredValue(submodule) { const setCookie = cookieSetter(submodule, coreStorage); const expiry = (new Date(Date.now() - 1000 * 60 * 60 * 24)).toUTCString(); deleter = (suffix) => setCookie(suffix, '', expiry) - suffixes = ['', '_last']; + suffixes = ['', '_last', '_cst']; break; case LOCAL_STORAGE: deleter = (suffix) => coreStorage.removeDataFromLocalStorage(submodule.config.storage.name + suffix) - suffixes = ['', '_last', '_exp']; + suffixes = ['', '_last', '_exp', '_cst']; break; } if (deleter) { @@ -338,75 +339,11 @@ function getStoredValue(submodule, key = undefined) { return storedValue; } -/** - * makes an object that can be stored with only the keys we need to check. - * excluding the vendorConsents object since the consentString is enough to know - * if consent has changed without needing to have all the details in an object - * @param consentData - * @returns {{apiVersion: number, gdprApplies: boolean, consentString: string}} - */ -function makeStoredConsentDataHash(consentData) { - const storedConsentData = { - consentString: '', - gdprApplies: false, - apiVersion: 0 - }; - - if (consentData) { - storedConsentData.consentString = consentData.consentString; - storedConsentData.gdprApplies = consentData.gdprApplies; - storedConsentData.apiVersion = consentData.apiVersion; - } - - return cyrb53Hash(JSON.stringify(storedConsentData)); -} - -/** - * puts the current consent data into cookie storage - * @param consentData - */ -export function setStoredConsentData(consentData) { - try { - const expiresStr = (new Date(Date.now() + (CONSENT_DATA_COOKIE_STORAGE_CONFIG.expires * (60 * 60 * 24 * 1000)))).toUTCString(); - coreStorage.setCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name, makeStoredConsentDataHash(consentData), expiresStr, 'Lax'); - } catch (error) { - logError(error); - } -} - -/** - * get the stored consent data from local storage, if any - * @returns {string} - */ -function getStoredConsentData() { - try { - return coreStorage.getCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name); - } catch (e) { - logError(e); - } -} - -/** - * test if the consent object stored locally matches the current consent data. if they - * don't match or there is nothing stored locally, it means a refresh of the user id - * submodule is needed - * @param storedConsentData - * @param consentData - * @returns {boolean} - */ -function storedConsentDataMatchesConsentData(storedConsentData, consentData) { - return ( - typeof storedConsentData !== 'undefined' && - storedConsentData !== null && - storedConsentData === makeStoredConsentDataHash(consentData) - ); -} - /** * @param {SubmoduleContainer[]} submodules * @param {function} cb - callback for after processing is done. */ -function processSubmoduleCallbacks(submodules, cb) { +function processSubmoduleCallbacks(submodules, cb, allModules) { cb = uidMetrics().fork().startTiming('userId.callbacks.total').stopBefore(cb); const done = delayExecution(() => { clearTimeout(timeoutID); @@ -422,7 +359,7 @@ function processSubmoduleCallbacks(submodules, cb) { } // cache decoded value (this is copied to every adUnit bid) submodule.idObj = submodule.submodule.decode(idObj, submodule.config); - updatePPID(submodule.idObj); + updatePPID(getCombinedSubmoduleIds(allModules)); } else { logInfo(`${MODULE_NAME}: ${submodule.submodule.name} - request id responded with an empty value`); } @@ -447,14 +384,7 @@ function getCombinedSubmoduleIds(submodules) { if (!Array.isArray(submodules) || !submodules.length) { return {}; } - const combinedSubmoduleIds = submodules.filter(i => isPlainObject(i.idObj) && Object.keys(i.idObj).length).reduce((carry, i) => { - Object.keys(i.idObj).forEach(key => { - carry[key] = i.idObj[key]; - }); - return carry; - }, {}); - - return combinedSubmoduleIds; + return getPrioritizedCombinedSubmoduleIds(submodules) } /** @@ -466,9 +396,14 @@ function getSubmoduleId(submodules, sourceName) { if (!Array.isArray(submodules) || !submodules.length) { return {}; } - const submodule = submodules.filter(sub => isPlainObject(sub.idObj) && - Object.keys(sub.idObj).length && USER_IDS_CONFIG[Object.keys(sub.idObj)[0]]?.source === sourceName); - return !isEmpty(submodule) ? submodule[0].idObj : []; + + const prioritisedIds = getPrioritizedCombinedSubmoduleIds(submodules); + const eligibleIdName = Object.keys(prioritisedIds).find(idName => { + const config = EID_CONFIG.get(idName); + return config?.source === sourceName || (isFn(config?.getSource) && config.getSource() === sourceName); + }); + + return eligibleIdName ? {[eligibleIdName]: prioritisedIds[eligibleIdName]} : []; } /** @@ -480,15 +415,39 @@ function getCombinedSubmoduleIdsForBidder(submodules, bidder) { if (!Array.isArray(submodules) || !submodules.length || !bidder) { return {}; } - return submodules + const eligibleSubmodules = submodules .filter(i => !i.config.bidders || !isArray(i.config.bidders) || includes(i.config.bidders, bidder)) - .filter(i => isPlainObject(i.idObj) && Object.keys(i.idObj).length) - .reduce((carry, i) => { - Object.keys(i.idObj).forEach(key => { - carry[key] = i.idObj[key]; - }); - return carry; - }, {}); + + return getPrioritizedCombinedSubmoduleIds(eligibleSubmodules); +} + +function collectByPriority(submodules, getIds, getName) { + return Object.fromEntries(Object.entries(submodules.reduce((carry, submod) => { + const ids = getIds(submod); + ids && Object.keys(ids).forEach(key => { + const maybeCurrentIdPriority = idPriority[key]?.indexOf(getName(submod)); + const currentIdPriority = isNumber(maybeCurrentIdPriority) ? maybeCurrentIdPriority : -1; + const currentIdState = {priority: currentIdPriority, value: ids[key]}; + if (carry[key]) { + const winnerIdState = currentIdState.priority > carry[key].priority ? currentIdState : carry[key]; + carry[key] = winnerIdState; + } else { + carry[key] = currentIdState; + } + }); + return carry; + }, {})).map(([k, v]) => [k, v.value])); +} + +/** + * @param {SubmoduleContainer[]} submodules + */ +function getPrioritizedCombinedSubmoduleIds(submodules) { + return collectByPriority( + submodules.filter(i => isPlainObject(i.idObj) && Object.keys(i.idObj).length), + (submod) => submod.idObj, + (submod) => submod.submodule.name + ) } /** @@ -547,21 +506,21 @@ function idSystemInitializer({delay = GreedyPromise.timeout} = {}) { } } - function timeGdpr() { - return gdprDataHandler.promise.finally(initMetrics.startTiming('userId.init.gdpr')); + function timeConsent() { + return allConsent.promise.finally(initMetrics.startTiming('userId.init.consent')) } let done = cancelAndTry( GreedyPromise.all([hooksReady, startInit.promise]) - .then(timeGdpr) - .then(checkRefs((consentData) => { - initSubmodules(initModules, allModules, consentData); + .then(timeConsent) + .then(checkRefs(() => { + initSubmodules(initModules, allModules); })) .then(() => startCallbacks.promise.finally(initMetrics.startTiming('userId.callbacks.pending'))) .then(checkRefs(() => { const modWithCb = initModules.filter(item => isFn(item.callback)); if (modWithCb.length) { - return new GreedyPromise((resolve) => processSubmoduleCallbacks(modWithCb, resolve)); + return new GreedyPromise((resolve) => processSubmoduleCallbacks(modWithCb, resolve, initModules)); } })) ); @@ -589,18 +548,17 @@ function idSystemInitializer({delay = GreedyPromise.timeout} = {}) { done = cancelAndTry( done .catch(() => null) - .then(timeGdpr) // fetch again in case a refresh was forced before this was resolved - .then(checkRefs((consentData) => { + .then(timeConsent) // fetch again in case a refresh was forced before this was resolved + .then(checkRefs(() => { const cbModules = initSubmodules( initModules, allModules.filter((sm) => submoduleNames == null || submoduleNames.includes(sm.submodule.name)), - consentData, true ).filter((sm) => { return sm.callback != null; }); if (cbModules.length) { - return new GreedyPromise((resolve) => processSubmoduleCallbacks(cbModules, resolve)); + return new GreedyPromise((resolve) => processSubmoduleCallbacks(cbModules, resolve, initModules)); } })) ); @@ -716,14 +674,14 @@ function registerSignalSources() { if (!isGptPubadsDefined()) { return; } - window.googletag.encryptedSignalProviders = window.googletag.encryptedSignalProviders || []; + window.googletag.secureSignalProviders = window.googletag.secureSignalProviders || []; const encryptedSignalSources = config.getConfig('userSync.encryptedSignalSources'); if (encryptedSignalSources) { const registerDelay = encryptedSignalSources.registerDelay || 0; setTimeout(() => { encryptedSignalSources['sources'] && encryptedSignalSources['sources'].forEach(({ source, encrypt, customFunc }) => { source.forEach((src) => { - window.googletag.encryptedSignalProviders.push({ + window.googletag.secureSignalProviders.push({ id: src, collectorFunction: () => getEncryptedEidsForSource(src, encrypt, customFunc) }); @@ -783,7 +741,27 @@ function getUserIdsAsync() { ); } -function populateSubmoduleId(submodule, consentData, storedConsentData, forceRefresh) { +export function getConsentHash() { + // transform decimal string into base64 to save some space on cookies + let hash = Number(allConsent.hash); + const bytes = []; + while (hash > 0) { + bytes.push(String.fromCharCode(hash & 255)); + hash = hash >>> 8; + } + return btoa(bytes.join()); +} + +function consentChanged(submodule) { + const storedConsent = getStoredValue(submodule, 'cst'); + return !storedConsent || storedConsent !== getConsentHash(); +} + +function populateSubmoduleId(submodule, forceRefresh, allSubmodules) { + // TODO: the ID submodule API only takes GDPR consent; it should be updated now that GDPR + // is only a tiny fraction of a vast consent universe + const gdprConsent = gdprDataHandler.getConsentData(); + // There are two submodule configuration types to handle: storage or value // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method // 2. value: pass directly to bids @@ -797,12 +775,12 @@ function populateSubmoduleId(submodule, consentData, storedConsentData, forceRef refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000); } - if (!storedId || refreshNeeded || forceRefresh || !storedConsentDataMatchesConsentData(storedConsentData, consentData)) { + if (!storedId || refreshNeeded || forceRefresh || consentChanged(submodule)) { // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. - response = submodule.submodule.getId(submodule.config, consentData, storedId); + response = submodule.submodule.getId(submodule.config, gdprConsent, storedId); } else if (typeof submodule.submodule.extendId === 'function') { // If the id exists already, give submodule a chance to decide additional actions that need to be taken - response = submodule.submodule.extendId(submodule.config, consentData, storedId); + response = submodule.submodule.extendId(submodule.config, gdprConsent, storedId); } if (isPlainObject(response)) { @@ -826,13 +804,13 @@ function populateSubmoduleId(submodule, consentData, storedConsentData, forceRef // cache decoded value (this is copied to every adUnit bid) submodule.idObj = submodule.config.value; } else { - const response = submodule.submodule.getId(submodule.config, consentData, undefined); + const response = submodule.submodule.getId(submodule.config, gdprConsent, undefined); if (isPlainObject(response)) { if (typeof response.callback === 'function') { submodule.callback = response.callback; } if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config); } } } - updatePPID(submodule.idObj); + updatePPID(getCombinedSubmoduleIds(allSubmodules)); } function updatePPID(userIds = getUserIds()) { @@ -852,7 +830,7 @@ function updatePPID(userIds = getUserIds()) { } } -function initSubmodules(dest, submodules, consentData, forceRefresh = false) { +function initSubmodules(dest, submodules, forceRefresh = false) { return uidMetrics().fork().measureTime('userId.init.modules', function () { if (!submodules.length) return []; // to simplify log messages from here on @@ -872,14 +850,10 @@ function initSubmodules(dest, submodules, consentData, forceRefresh = false) { return []; } - // we always want the latest consentData stored, even if we don't execute any submodules - const storedConsentData = getStoredConsentData(); - setStoredConsentData(consentData); - const initialized = submodules.reduce((carry, submodule) => { return submoduleMetrics(submodule.submodule.name).measureTime('init', () => { try { - populateSubmoduleId(submodule, consentData, storedConsentData, forceRefresh); + populateSubmoduleId(submodule, forceRefresh, submodules); carry.push(submodule); } catch (e) { logError(`Error in userID module '${submodule.submodule.name}':`, e); @@ -970,10 +944,20 @@ function canUseStorage(submodule) { return false; } +function updateEIDConfig(submodules) { + EID_CONFIG.clear(); + Object.entries(collectByPriority( + submodules, + (mod) => mod.eids, + (mod) => mod.name + )).forEach(([id, conf]) => EID_CONFIG.set(id, conf)); +} + /** * update submodules by validating against existing configs and storage types */ function updateSubmodules() { + updateEIDConfig(submoduleRegistry); const configs = getValidSubmoduleConfigs(configRegistry, submoduleRegistry); if (!configs.length) { return; @@ -1010,6 +994,25 @@ function updateSubmodules() { } } +/** + * This function will update the idPriority according to the provided configuration + * @param {Object} idPriorityConfig + * @param {SubmoduleContainer[]} submodules + */ +function updateIdPriority(idPriorityConfig, submodules) { + if (idPriorityConfig) { + const result = {}; + const aliasToName = new Map(submodules.map(s => s.submodule.aliasName ? [s.submodule.aliasName, s.submodule.name] : [])); + Object.keys(idPriorityConfig).forEach(key => { + const priority = isArray(idPriorityConfig[key]) ? [...idPriorityConfig[key]].reverse() : [] + result[key] = priority.map(s => aliasToName.has(s) ? aliasToName.get(s) : s); + }); + idPriority = result; + } else { + idPriority = {}; + } +} + export function requestDataDeletion(next, ...args) { logInfo('UserID: received data deletion request; deleting all stored IDs...') submodules.forEach(submodule => { @@ -1069,13 +1072,16 @@ export function init(config, {delay = GreedyPromise.timeout} = {}) { configListener = config.getConfig('userSync', conf => { // Note: support for 'usersync' was dropped as part of Prebid.js 4.0 const userSync = conf.userSync; - ppidSource = userSync.ppid; - if (userSync && userSync.userIds) { - configRegistry = userSync.userIds; - syncDelay = isNumber(userSync.syncDelay) ? userSync.syncDelay : DEFAULT_SYNC_DELAY; - auctionDelay = isNumber(userSync.auctionDelay) ? userSync.auctionDelay : NO_AUCTION_DELAY; - updateSubmodules(); - initIdSystem({ready: true}); + if (userSync) { + ppidSource = userSync.ppid; + if (userSync.userIds) { + configRegistry = userSync.userIds; + syncDelay = isNumber(userSync.syncDelay) ? userSync.syncDelay : DEFAULT_SYNC_DELAY; + auctionDelay = isNumber(userSync.auctionDelay) ? userSync.auctionDelay : NO_AUCTION_DELAY; + updateSubmodules(); + updateIdPriority(userSync.idPriority, submodules); + initIdSystem({ready: true}); + } } }); diff --git a/modules/userId/userId.md b/modules/userId/userId.md index 15f609ddced..01930a67dda 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -5,6 +5,9 @@ Example showing `cookie` storage for user id data for each of the submodules ``` pbjs.setConfig({ userSync: { + idPriority: { + uid2: ['uid2', 'liveIntentId'] + } userIds: [{ name: "33acrossId", storage: { diff --git a/modules/utiqSystem.js b/modules/utiqSystem.js index 441bba5660f..473dc5854a9 100644 --- a/modules/utiqSystem.js +++ b/modules/utiqSystem.js @@ -124,6 +124,15 @@ export const utiqSubmodule = { return { callback: result }; } }, + eids: { + 'utiq': { + source: 'utiq.com', + atype: 1, + getValue: function (data) { + return data; + }, + }, + } }; submodule('userId', utiqSubmodule); diff --git a/modules/vdoaiBidAdapter.js b/modules/vdoaiBidAdapter.js index fa1e2898a4a..05960378d23 100644 --- a/modules/vdoaiBidAdapter.js +++ b/modules/vdoaiBidAdapter.js @@ -1,6 +1,6 @@ -import {getAdUnitSizes} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'vdoai'; const ENDPOINT_URL = 'https://prebid.vdo.ai/auction'; diff --git a/modules/ventesBidAdapter.js b/modules/ventesBidAdapter.js index 73ae0a7e5f1..78c580c4116 100644 --- a/modules/ventesBidAdapter.js +++ b/modules/ventesBidAdapter.js @@ -1,8 +1,9 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {convertCamelToUnderscore, isArray, isNumber, isPlainObject, isStr, replaceAuctionPrice} from '../src/utils.js'; +import {isArray, isNumber, isPlainObject, isStr, replaceAuctionPrice} from '../src/utils.js'; import {find} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; const BID_METHOD = 'POST'; const BIDDER_URL = 'https://ad.ventesavenues.in/va/ad'; diff --git a/modules/verizonMediaIdSystem.js b/modules/verizonMediaIdSystem.js index 27577ad0de4..6b22538fdc7 100644 --- a/modules/verizonMediaIdSystem.js +++ b/modules/verizonMediaIdSystem.js @@ -99,6 +99,12 @@ export const verizonMediaIdSubmodule = { */ getAjaxFn() { return ajax; + }, + eids: { + 'connectid': { + source: 'verizonmedia.com', + atype: 3 + }, } }; diff --git a/modules/viantOrtbBidAdapter.js b/modules/viantOrtbBidAdapter.js new file mode 100644 index 00000000000..e7bf9129a62 --- /dev/null +++ b/modules/viantOrtbBidAdapter.js @@ -0,0 +1,113 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' +import {deepAccess, getBidIdParameter, logError} from '../src/utils.js'; + +const BIDDER_CODE = 'viant'; +const ENDPOINT = 'https://bidders-us-east-1.adelphic.net/d/rtb/v25/prebid/bidder_test' + +const DEFAULT_BID_TTL = 300; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: function (bid) { + if (bid && typeof bid.params !== 'object') { + logError(BIDDER_CODE + ': params is not defined or is incorrect in the bidder settings.'); + return false; + } + if (!getBidIdParameter('publisherId', bid.params)) { + logError(BIDDER_CODE + ': publisherId is not present in bidder params.'); + return false; + } + const mediaTypesBanner = deepAccess(bid, 'mediaTypes.banner'); + const mediaTypesVideo = deepAccess(bid, 'mediaTypes.video'); + const mediaTypesNative = deepAccess(bid, 'mediaTypes.native'); + if (!mediaTypesBanner && !mediaTypesVideo && !mediaTypesNative) { + utils.logWarn(BIDDER_CODE + ': one of mediaTypes.banner or mediaTypes.video or mediaTypes.native must be passed'); + return false; + } + return true; + }, + + buildRequests, + + interpretResponse(response, request) { + if (!response.body) { + response.body = {nbr: 0}; + } + const bids = converter.fromORTB({request: request.data, response: response.body}).bids; + return bids; + }, + + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} bid The bid that won the auction + */ + onBidWon: function (bid) { + if (bid.burl) { + utils.triggerPixel(bid.burl); + } else if (bid.nurl) { + utils.triggerPixel(bid.nurl); + } + } +} + +function buildRequests(bids, bidderRequest) { + let videoBids = bids.filter(bid => isVideoBid(bid)); + let bannerBids = bids.filter(bid => isBannerBid(bid)); + let nativeBids = bids.filter(bid => isNativeBid(bid)); + let requests = bannerBids.length ? [createRequest(bannerBids, bidderRequest, BANNER)] : []; + videoBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + nativeBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, NATIVE)); + }); + return requests; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + const data = converter.toORTB({bidRequests, bidderRequest, context: {mediaType}}); + if (bidderRequest.gdprConsent && typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + if (!data.regs) data.regs = {}; + if (!data.regs.ext) data.regs.ext = {}; + data.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + if (bidderRequest.uspConsent) { + if (!data.regs) data.regs = {}; + if (!data.regs.ext) data.regs.ext = {}; + data.regs.ext.us_privacy = bidderRequest.uspConsent; + } + return { + method: 'POST', + url: ENDPOINT, + data: data + } +} + +function isVideoBid(bid) { + return deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return deepAccess(bid, 'mediaTypes.banner'); +} + +function isNativeBid(bid) { + return deepAccess(bid, 'mediaTypes.native'); +} + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY + } +}); + +registerBidder(spec); diff --git a/modules/viantOrtbBidAdapter.md b/modules/viantOrtbBidAdapter.md new file mode 100644 index 00000000000..def93722b7b --- /dev/null +++ b/modules/viantOrtbBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + +Module Name: VIANT Bidder Adapter +Module Type: Bidder Adapter +Maintainer: Marketplace@adelphic.com + +# Description + +An adapter to get a bid from VIANT DSP. + +# Test Parameters +```javascript + var adUnits = [ // Banner adUnit with only required parameters + { + code: 'test-div-minimal', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bids: [ + { + bidder: 'viant', + params: { + supplySourceId: 'supplier', + publisherId: '464' + } + } + ] + }, + { + code: 'test-div-minimal-video', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'outstream' + } + }, + bids: [ + { + bidder: 'viant', + params: { + supplySourceId: 'supplier', + publisherId: '464' // required + } + } + ] + } + ]; +``` + +Where: + +* placementId - Placement ID of the ad unit (required) diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index 8341eb332d2..b5323181c6c 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -1,9 +1,10 @@ -import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {_each, deepAccess, isFn, parseSizesInput, parseUrl, uniques, isArray} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; import {bidderSettings} from '../src/bidderSettings.js'; import {config} from '../src/config.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const GVLID = 744; const DEFAULT_SUB_DOMAIN = 'prebid'; @@ -48,7 +49,7 @@ function isBidRequestValid(bid) { return !!(extractCID(params) && extractPID(params)); } -function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { +function buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { const { params, bidId, @@ -56,7 +57,6 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { adUnitCode, schain, mediaTypes, - auctionId, ortb2Imp, bidderRequestId, bidRequestsCount, @@ -69,9 +69,7 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { const dealId = getNextDealId(hashUrl); const uniqueDealId = getUniqueDealId(hashUrl); const sId = getVidazooSessionId(); - const cId = extractCID(params); const pId = extractPID(params); - const subDomain = extractSubDomain(params); const ptrace = getCacheOpt(); const isStorageAllowed = bidderSettings.get(BIDDER_CODE, 'storageAllowed'); @@ -114,8 +112,6 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { gpid: gpid, cat: cat, pagecat: pagecat, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: auctionId, transactionId: ortb2Imp?.ext?.tid, bidderRequestId: bidderRequestId, bidRequestsCount: bidRequestsCount, @@ -153,17 +149,46 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { data.gppSid = bidderRequest.ortb2.regs.gpp_sid; } + _each(ext, (value, key) => { + data['ext.' + key] = value; + }); + + return data; +} + +function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const {params} = bid; + const cId = extractCID(params); + const subDomain = extractSubDomain(params); + const data = buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout); const dto = { method: 'POST', url: `${createDomain(subDomain)}/prebid/multi/${cId}`, data: data }; + return dto; +} - _each(ext, (value, key) => { - dto.data['ext.' + key] = value; +function buildSingleRequest(bidRequests, bidderRequest, topWindowUrl, bidderTimeout) { + const {params} = bidRequests[0]; + const cId = extractCID(params); + const subDomain = extractSubDomain(params); + const data = bidRequests.map(bid => { + const sizes = parseSizesInput(bid.sizes); + return buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) + }); + const chunkSize = Math.min(20, config.getConfig('vidazoo.chunkSize') || 10); + + const chunkedData = chunk(data, chunkSize); + return chunkedData.map(chunk => { + return { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: { + bids: chunk + } + }; }); - - return dto; } function appendUserIdsToRequestPayload(payloadRef, userIds) { @@ -193,12 +218,34 @@ function buildRequests(validBidRequests, bidderRequest) { // TODO: does the fallback make sense here? const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; const bidderTimeout = config.getConfig('bidderTimeout'); + + const singleRequestMode = config.getConfig('vidazoo.singleRequest'); + const requests = []; - validBidRequests.forEach(validBidRequest => { - const sizes = parseSizesInput(validBidRequest.sizes); - const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); - requests.push(request); - }); + + if (singleRequestMode) { + // banner bids are sent as a single request + const bannerBidRequests = validBidRequests.filter(bid => isArray(bid.mediaTypes) ? bid.mediaTypes.includes(BANNER) : bid.mediaTypes[BANNER] !== undefined); + if (bannerBidRequests.length > 0) { + const singleRequests = buildSingleRequest(bannerBidRequests, bidderRequest, topWindowUrl, bidderTimeout); + requests.push(...singleRequests); + } + + // video bids are sent as a single request for each bid + + const videoBidRequests = validBidRequests.filter(bid => bid.mediaTypes[VIDEO] !== undefined); + videoBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + } else { + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + } return requests; } @@ -206,13 +253,15 @@ function interpretResponse(serverResponse, request) { if (!serverResponse || !serverResponse.body) { return []; } - const {bidId} = request.data; + + const singleRequestMode = config.getConfig('vidazoo.singleRequest'); + const reqBidId = deepAccess(request, 'data.bidId'); const {results} = serverResponse.body; let output = []; try { - results.forEach(result => { + results.forEach((result, i) => { const { creativeId, ad, @@ -221,6 +270,7 @@ function interpretResponse(serverResponse, request) { width, height, currency, + bidId, advertiserDomains, metaData, mediaType = BANNER @@ -230,7 +280,7 @@ function interpretResponse(serverResponse, request) { } const response = { - requestId: bidId, + requestId: (singleRequestMode && bidId) ? bidId : reqBidId, cpm: price, width: width, height: height, @@ -264,19 +314,27 @@ function interpretResponse(serverResponse, request) { } output.push(response); }); + return output; } catch (e) { return []; } } -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { let syncs = []; const {iframeEnabled, pixelEnabled} = syncOptions; const {gdprApplies, consentString = ''} = gdprConsent; + const {gppString, applicableSections} = gppConsent; const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}`; + + if (gppString && applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppString); + params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); + } + if (iframeEnabled) { syncs.push({ type: 'iframe', diff --git a/modules/videojsVideoProvider.js b/modules/videojsVideoProvider.js index 23a23447faa..7764e8af995 100644 --- a/modules/videojsVideoProvider.js +++ b/modules/videojsVideoProvider.js @@ -201,8 +201,15 @@ export function VideojsProvider(providerConfig, vjs_, adState_, timeState_, call return; } - player.ima.changeAdTag(adTagUrl); - player.ima.requestAds(); + // The VideoJS IMA plugin version 1.11.0 will throw when the ad is empty. + try { + player.ima.changeAdTag(adTagUrl); + player.ima.requestAds(); + } catch (e) { + /* + Handling is not required; ad errors are emitted automatically by video.js + */ + } } function onEvent(type, callback, payload) { diff --git a/modules/videoreachBidAdapter.js b/modules/videoreachBidAdapter.js index 9fd5853c75e..8835398d7cc 100644 --- a/modules/videoreachBidAdapter.js +++ b/modules/videoreachBidAdapter.js @@ -1,4 +1,4 @@ -import { getValue, getBidIdParameter } from '../src/utils.js'; +import {getBidIdParameter, getValue} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'videoreach'; const ENDPOINT_URL = 'https://a.videoreach.com/hb/'; diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index a066d93d69e..a45a1db9ece 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -1,8 +1,11 @@ -import { triggerPixel, parseSizesInput, deepAccess, logError, getGptSlotInfoForAdUnitCode } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { INSTREAM as VIDEO_INSTREAM } from '../src/video.js'; +import {deepAccess, logError, parseSizesInput, triggerPixel} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {INSTREAM as VIDEO_INSTREAM} from '../src/video.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; + const BIDDER_CODE = 'visx'; const GVLID = 154; const BASE_URL = 'https://t.visx.net'; @@ -29,6 +32,7 @@ const LOG_ERROR_MESS = { videoMissing: 'Bid request videoType property is missing - ' }; const currencyWhiteList = ['EUR', 'USD', 'GBP', 'PLN']; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -117,10 +121,13 @@ export const spec = { ...(payloadSchain && { schain: payloadSchain }) } }; + + const vads = _getUserId(); const user = { ext: { ...(payloadUserEids && { eids: payloadUserEids }), - ...(payload.gdpr_consent && { consent: payload.gdpr_consent }) + ...(payload.gdpr_consent && { consent: payload.gdpr_consent }), + ...(vads && { vads }) } }; const regs = ('gdpr_applies' in payload) && { @@ -382,4 +389,49 @@ function _isAdSlotExists(adUnitCode) { return false; } +// Generate user id (25 chars) with NanoID +// https://github.com/ai/nanoid/ +function _generateUserId() { + for ( + var t = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict', + e = new Date().getTime() % 1073741824, + i = '', + o = 0; + o < 5; + o++ + ) { + i += t[e % 64]; + e = Math.floor(e / 64); + } + for (o = 20; o--;) i += t[(64 * Math.random()) | 0]; + return i; +} + +function _getUserId() { + const USER_ID_KEY = '__vads'; + let vads; + + if (storage.cookiesAreEnabled()) { + vads = storage.getCookie(USER_ID_KEY); + } else if (storage.localStorageIsEnabled()) { + vads = storage.getDataFromLocalStorage(USER_ID_KEY); + } + + if (vads && vads.length) { + return vads; + } + + vads = _generateUserId(); + if (storage.cookiesAreEnabled()) { + const expires = new Date(Date.now() + 2592e6).toUTCString(); + storage.setCookie(USER_ID_KEY, vads, expires); + return vads; + } else if (storage.localStorageIsEnabled()) { + storage.setDataInLocalStorage(USER_ID_KEY, vads); + return vads; + } + + return null; +} + registerBidder(spec); diff --git a/modules/voxBidAdapter.js b/modules/voxBidAdapter.js index acf1cc9579c..34bd46ccb98 100644 --- a/modules/voxBidAdapter.js +++ b/modules/voxBidAdapter.js @@ -4,6 +4,9 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {find} from '../src/polyfill.js'; import {auctionManager} from '../src/auctionManager.js'; import {Renderer} from '../src/Renderer.js'; +import {config} from '../src/config.js' + +const { getConfig } = config; const BIDDER_CODE = 'vox'; const SSP_ENDPOINT = 'https://ssp.hybrid.ai/auction/prebid'; @@ -11,13 +14,21 @@ const VIDEO_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVi const TTL = 60; function buildBidRequests(validBidRequests) { - return _map(validBidRequests, function(validBidRequest) { - const params = validBidRequest.params; + return _map(validBidRequests, function(bid) { + const currency = getConfig('currency.adServerCurrency'); + const floorInfo = bid.getFloor ? bid.getFloor({ + currency: currency || 'USD' + }) : {}; + + const params = bid.params; const bidRequest = { - bidId: validBidRequest.bidId, + floorInfo, + schain: bid.schain, + userId: bid.userId, + bidId: bid.bidId, // TODO: fix transactionId leak: https://github.com/prebid/Prebid.js/issues/9781 - transactionId: validBidRequest.transactionId, - sizes: validBidRequest.sizes, + transactionId: bid.transactionId, + sizes: bid.sizes, placement: params.placement, placeId: params.placementId, imageUrl: params.imageUrl diff --git a/modules/vrtcalBidAdapter.js b/modules/vrtcalBidAdapter.js index ff7839d0e02..07dc35525c3 100644 --- a/modules/vrtcalBidAdapter.js +++ b/modules/vrtcalBidAdapter.js @@ -1,11 +1,16 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import {ajax} from '../src/ajax.js'; -import {isFn, isPlainObject} from '../src/utils.js'; import { config } from '../src/config.js'; +import {deepAccess, isFn, isPlainObject} from '../src/utils.js'; + +const GVLID = 706; +const VRTCAL_USER_SYNC_URL_IFRAME = `https://usync.vrtcal.com/i?ssp=1804&synctype=iframe`; +const VRTCAL_USER_SYNC_URL_REDIRECT = `https://usync.vrtcal.com/i?ssp=1804&synctype=redirect`; export const spec = { code: 'vrtcal', + gvlid: GVLID, supportedMediaTypes: [BANNER], isBidRequestValid: function (bid) { return true; @@ -62,13 +67,14 @@ export const spec = { site: { id: 'VRTCAL_FILLED', name: 'VRTCAL_FILLED', - cat: ['VRTCAL_FILLED'], - domain: decodeURIComponent(window.location.href).replace('https://', '').replace('http://', '').split('/')[0] - + cat: deepAccess(bid, 'ortb2.site.cat', []), + domain: decodeURIComponent(window.location.href).replace('https://', '').replace('http://', '').split('/')[0], + page: window.location.href }, device: { - ua: 'VRTCAL_FILLED', - ip: 'VRTCAL_FILLED' + language: navigator.language, + ua: navigator.userAgent, + ip: deepAccess(bid, 'params.bidOverride.device.ip') || deepAccess(bid, 'params.ext.ip') || undefined }, regs: { coppa: coppa, @@ -144,7 +150,34 @@ export const spec = { ); ajax(winUrl, null); return true; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { + const syncs = []; + const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; + const usPrivacy = `&us_privacy=${encodeURIComponent(uspConsent)}`; + const gpp = gppConsent.gppString ? gppConsent.gppString : ''; + const gppSid = Array.isArray(gppConsent.applicableSections) ? gppConsent.applicableSections.join(',') : ''; + let vrtcalSyncURL = '' + + if (syncOptions.iframeEnabled) { + vrtcalSyncURL = `${VRTCAL_USER_SYNC_URL_IFRAME}${usPrivacy}${gdprFlag}${gdprString}&gpp=${gpp}&gpp_sid=${gppSid}&surl=`; + syncs.push({ + type: 'iframe', + url: vrtcalSyncURL + }); + } else { + vrtcalSyncURL = `${VRTCAL_USER_SYNC_URL_REDIRECT}${usPrivacy}${gdprFlag}${gdprString}&gpp=${gpp}&gpp_sid=${gppSid}&surl=`; + syncs.push({ + type: 'image', + url: vrtcalSyncURL + }); + } + + return syncs; } + }; registerBidder(spec); diff --git a/modules/weboramaRtdProvider.js b/modules/weboramaRtdProvider.js index 6ba502d2c8b..14abba95323 100644 --- a/modules/weboramaRtdProvider.js +++ b/modules/weboramaRtdProvider.js @@ -109,8 +109,7 @@ import { isBoolean, isPlainObject, logWarn, - mergeDeep, - tryAppendQueryString + mergeDeep } from '../src/utils.js'; import { submodule @@ -123,6 +122,7 @@ import { } from '../src/storageManager.js'; import adapterManager from '../src/adapterManager.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; diff --git a/modules/winrBidAdapter.js b/modules/winrBidAdapter.js index 5cc063d16b4..41efc432e11 100644 --- a/modules/winrBidAdapter.js +++ b/modules/winrBidAdapter.js @@ -1,6 +1,4 @@ import { - convertCamelToUnderscore, - convertTypes, deepAccess, getBidRequest, getParameterByName, @@ -16,7 +14,9 @@ import {BANNER} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; import {getStorageManager} from '../src/storageManager.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'winr'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; diff --git a/modules/xeBidAdapter.js b/modules/xeBidAdapter.js index 94a66e9980e..6f527d905d6 100644 --- a/modules/xeBidAdapter.js +++ b/modules/xeBidAdapter.js @@ -1,7 +1,8 @@ import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { getAdUnitSizes, parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray } from '../src/utils.js'; +import {parseSizesInput, isFn, deepAccess, logError, isArray, getBidIdParameter} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const CUR = 'USD'; const BIDDER_CODE = 'xe'; diff --git a/modules/yahoosspBidAdapter.js b/modules/yahoosspBidAdapter.js index 7a414c0d2ee..a66d76f8689 100644 --- a/modules/yahoosspBidAdapter.js +++ b/modules/yahoosspBidAdapter.js @@ -6,9 +6,10 @@ import { Renderer } from '../src/Renderer.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const INTEGRATION_METHOD = 'prebid.js'; -const BIDDER_CODE = 'yahoossp'; +const BIDDER_CODE = 'yahooAds'; +const BIDDER_ALIASES = ['yahoossp', 'yahooAdvertising'] const GVLID = 25; -const ADAPTER_VERSION = '1.0.2'; +const ADAPTER_VERSION = '1.1.0'; const PREBID_VERSION = '$prebid.version$'; const DEFAULT_BID_TTL = 300; const TEST_MODE_DCN = '8a969516017a7a396ec539d97f540011'; @@ -49,13 +50,17 @@ const SUPPORTED_USER_ID_SOURCES = [ 'quantcast.com', 'tapad.com', 'uidapi.com', - 'verizonmedia.com', 'yahoo.com', 'zeotap.com' ]; /* Utility functions */ +function getConfigValue(bid, key) { + const bidderCode = bid.bidder || bid.bidderCode; + return config.getConfig(`${bidderCode}.${key}`); +} + function getSize(size) { return { w: parseInt(size[0]), @@ -114,11 +119,12 @@ function extractUserSyncUrls(syncOptions, pixels) { */ function updateConsentQueryParams(url, consentData) { const parameterMap = { - 'gdpr_consent': consentData.gdpr.consentString, - 'gdpr': consentData.gdpr.gdprApplies ? '1' : '0', - 'us_privacy': consentData.uspConsent, - 'gpp': consentData.gpp.gppString, - 'gpp_sid': consentData.gpp.applicableSections ? consentData.gpp.applicableSections.join(',') : '' + 'gdpr_consent': consentData.gdpr ? consentData.gdpr.consentString : '', + 'gdpr': consentData.gdpr && consentData.gdpr.gdprApplies ? '1' : '0', + 'us_privacy': consentData.uspConsent ? consentData.uspConsent : '', + 'gpp': consentData.gpp ? consentData.gpp.gppString : '', + 'gpp_sid': consentData.gpp && Array.isArray(consentData.gpp.applicableSections) + ? consentData.gpp.applicableSections.join(',') : '' } const existingUrl = new URL(url); @@ -155,8 +161,8 @@ function getPubIdMode(bid) { return pubIdMode; }; -function getAdapterMode() { - let adapterMode = config.getConfig('yahoossp.mode'); +function getAdapterMode(bid) { + let adapterMode = getConfigValue(bid, 'mode'); adapterMode = adapterMode ? adapterMode.toLowerCase() : undefined; if (typeof adapterMode === 'undefined' || adapterMode === BANNER) { return BANNER; @@ -177,7 +183,7 @@ function getResponseFormat(bid) { }; function getFloorModuleData(bid) { - const adapterMode = getAdapterMode(); + const adapterMode = getAdapterMode(bid); const getFloorRequestObject = { currency: deepAccess(bid, 'params.bidOverride.cur') || DEFAULT_CURRENCY, mediaType: adapterMode, @@ -187,7 +193,7 @@ function getFloorModuleData(bid) { }; function filterBidRequestByMode(validBidRequests) { - const mediaTypesMode = getAdapterMode(); + const mediaTypesMode = getAdapterMode(validBidRequests[0]); let result = []; if (mediaTypesMode === BANNER) { result = validBidRequests.filter(bid => { @@ -244,7 +250,7 @@ function validateAppendObject(validationType, allowedKeys, inputObject, appendTo }; function getTtl(bidderRequest) { - const globalTTL = config.getConfig('yahoossp.ttl'); + const globalTTL = getConfigValue(bidderRequest, 'ttl'); return globalTTL ? validateTTL(globalTTL) : validateTTL(deepAccess(bidderRequest, 'params.ttl')); }; @@ -276,8 +282,8 @@ function generateOpenRtbObject(bidderRequest, bid) { ext: { 'us_privacy': bidderRequest.uspConsent ? bidderRequest.uspConsent : '', gdpr: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? 1 : 0, - gpp: bidderRequest.gppConsent.gppString, - gpp_sid: bidderRequest.gppConsent.applicableSections + gpp: bidderRequest.gppConsent ? bidderRequest.gppConsent.gppString : '', + gpp_sid: bidderRequest.gppConsent ? bidderRequest.gppConsent.applicableSections : [] } }, source: { @@ -332,7 +338,7 @@ function generateOpenRtbObject(bidderRequest, bid) { }; function appendImpObject(bid, openRtbObject) { - const mediaTypeMode = getAdapterMode(); + const mediaTypeMode = getAdapterMode(bid); if (openRtbObject && bid) { const impObject = { @@ -494,20 +500,21 @@ function appendFirstPartyData(outBoundBidRequest, bid) { function generateServerRequest({payload, requestOptions, bidderRequest}) { const pubIdMode = getPubIdMode(bidderRequest); - let sspEndpoint = config.getConfig('yahoossp.endpoint') || SSP_ENDPOINT_DCN_POS; + const overrideEndpoint = getConfigValue(bidderRequest, 'endpoint'); + let sspEndpoint = overrideEndpoint || SSP_ENDPOINT_DCN_POS; if (pubIdMode === true) { - sspEndpoint = config.getConfig('yahoossp.endpoint') || SSP_ENDPOINT_PUBID; + sspEndpoint = overrideEndpoint || SSP_ENDPOINT_PUBID; }; if (deepAccess(bidderRequest, 'params.testing.e2etest') === true) { - logInfo('yahoossp adapter e2etest mode is active'); + logInfo('Adapter e2etest mode is active'); requestOptions.withCredentials = false; if (pubIdMode === true) { payload.site.id = TEST_MODE_PUBID_DCN; } else { - const mediaTypeMode = getAdapterMode(); + const mediaTypeMode = getAdapterMode(bidderRequest); payload.site.id = TEST_MODE_DCN; payload.imp.forEach(impObject => { impObject.ext.e2eTestMode = true; @@ -516,8 +523,9 @@ function generateServerRequest({payload, requestOptions, bidderRequest}) { } else if (mediaTypeMode === VIDEO) { impObject.tagid = TEST_MODE_VIDEO_POS; // video passback } else { - logWarn('yahoossp adapter e2etest mode does not support yahoossp.mode="all". \n Please specify either "banner" or "video"'); - logWarn('yahoossp adapter e2etest mode: Please make sure your adUnit matches the yahoossp.mode video or banner'); + const bidderCode = bidderRequest.bidderCode; + logWarn(`e2etest mode does not support ${bidderCode}.mode="all". \n Please specify either "banner" or "video"`); + logWarn(`Adapter e2etest mode: Please make sure your adUnit matches the ${bidderCode}.mode video or banner`); } }); } @@ -528,7 +536,7 @@ function generateServerRequest({payload, requestOptions, bidderRequest}) { method: 'POST', data: payload, options: requestOptions, - bidderRequest: bidderRequest + bidderRequest // Additional data for use in interpretResponse() }; }; @@ -547,7 +555,7 @@ function createRenderer(bidderRequest, bidResponse) { }, deepAccess(bidderRequest, 'params.testing.renderer.setTimeout') || DEFAULT_RENDERER_TIMEOUT); }); } catch (error) { - logWarn('yahoossp renderer error: setRender() failed', error); + logWarn('Renderer error: setRender() failed', error); } return renderer; } @@ -557,7 +565,7 @@ function createRenderer(bidderRequest, bidResponse) { export const spec = { code: BIDDER_CODE, gvlid: GVLID, - aliases: [], + aliases: BIDDER_ALIASES, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bid) { @@ -570,14 +578,14 @@ export const spec = { ) { return true; } else { - logWarn('yahoossp bidder params missing or incorrect, please pass object with either: dcn & pos OR pubId'); + logWarn('Bidder params missing or incorrect, please pass object with either: dcn & pos OR pubId'); return false; } }, buildRequests: function(validBidRequests, bidderRequest) { if (isEmpty(validBidRequests) || isEmpty(bidderRequest)) { - logWarn('yahoossp Adapter: buildRequests called with either empty "validBidRequests" or "bidderRequest"'); + logWarn('buildRequests called with either empty "validBidRequests" or "bidderRequest"'); return undefined; }; @@ -592,13 +600,12 @@ export const spec = { const filteredBidRequests = filterBidRequestByMode(validBidRequests); - if (config.getConfig('yahoossp.singleRequestMode') === true) { + if (getConfigValue(bidderRequest, 'singleRequestMode') === true) { const payload = generateOpenRtbObject(bidderRequest, filteredBidRequests[0]); filteredBidRequests.forEach(bid => { appendImpObject(bid, payload); }); - - return generateServerRequest({payload, requestOptions, bidderRequest}); + return [generateServerRequest({payload, requestOptions, bidderRequest})]; } return filteredBidRequests.map(bid => { @@ -608,12 +615,11 @@ export const spec = { }); }, - interpretResponse: function(serverResponse, { data, bidderRequest }) { + interpretResponse: function(serverResponse, { bidderRequest }) { const response = []; if (!serverResponse.body || !Array.isArray(serverResponse.body.seatbid)) { return response; } - let seatbids = serverResponse.body.seatbid; seatbids.forEach(seatbid => { let bid; @@ -628,7 +634,6 @@ export const spec = { let bidResponse = { adId: deepAccess(bid, 'adId') ? bid.adId : bid.impid || bid.crid, - adUnitCode: bidderRequest.adUnitCode, requestId: bid.impid, cpm: cpm, width: bid.w, diff --git a/modules/yahoosspBidAdapter.md b/modules/yahoosspBidAdapter.md index 35a640b92b1..e7074e64682 100644 --- a/modules/yahoosspBidAdapter.md +++ b/modules/yahoosspBidAdapter.md @@ -1,10 +1,10 @@ # Overview -**Module Name:** yahoossp Bid Adapter +**Module Name:** Yahoo Advertising Bid Adapter **Module Type:** Bidder Adapter **Maintainer:** hb-fe-tech@yahooinc.com # Description -The Yahoo SSP Bid Adapter is an OpenRTB interface that consolidates all previous "Oath.inc" adapters such as: "aol", "oneMobile", "oneDisplay" & "oneVideo" supply-side platforms. +The Yahoo Advertising Bid Adapter is an OpenRTB interface that consolidates all previous "Oath.inc" adapters such as: "aol", "oneMobile", "oneDisplay" & "oneVideo" supply-side platforms. # Supported Features: * Media Types: Banner & Video @@ -19,51 +19,63 @@ The Yahoo SSP Bid Adapter is an OpenRTB interface that consolidates all previous * First Party Data (ortb2 & ortb2Imp) * Custom TTL (time to live) +# Adapter Aliases +Whilst the primary bidder code for this bid adapter is `yahooAds`, the aliases `yahoossp` and `yahooAdvertising` can be used to enable this adapter. If you wish to set Prebid configuration specifically for this bid adapter, then the configuration key _must_ match the used bidder code. All examples in this documentation use the primiry bidder code, but switching `yahooAds` with one of the relevant aliases may be required for your setup. Let's take [setting the request mode](#adapter-request-mode) as an example; if you used the `yahoossp` alias, then the corresponding `setConfig` API call would look like this: + +```javascript +pbjs.setConfig({ + yahoossp: { + mode: 'banner' // 'all', 'video', 'banner' (default) + } +}); +``` # Adapter Request mode -Since the yahoossp adapter now supports both Banner and Video adUnits a controller was needed to allow you to define when the adapter should generate a bid-requests to our Yahoo SSP. +Since the Yahoo Advertising bid adapter supports both Banner and Video adUnits, a controller was needed to allow you to define when the adapter should generate a bid-requests to the Yahoo bid endpoint. **Important!** By default the adapter mode is set to "banner" only. -This means that you do not need to explicitly declare the yahoossp.mode in the Global config to initiate banner adUnit requests. +This means that you do not need to explicitly declare the `yahooAds.mode` property in the global config to initiate banner adUnit requests. ## Request modes: * **undefined** - (Default) Will generate bid-requests for "Banner" formats only. * **banner** - Will generate bid-requests for "Banner" formats only (Explicit declaration). * **video** - Will generate bid-requests for "Video" formats only (Explicit declaration). -* **all** - Will generate bid-requests for both "Banner" & "Video" formats +* **all** - Will generate bid-requests for both "Banner" & "Video" formats. -**Important!** When setting yahoossp.mode = 'all' Make sure your Yahoo SSP Placement (pos id) supports both Banner & Video placements. -If it does not, the Yahoo SSP will respond only in the format it is set too. +**Important!** When setting `yahooAds.mode` to `'all'`, make sure your Yahoo Placement (pos id) supports both Banner & Video placements. +If it does not, the Yahoo bid server will respond only in the format it is set too. +### Example: explicitly setting the request mode ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'banner' // 'all', 'video', 'banner' (default) } }); ``` + # Integration Options -The `yahoossp` bid adapter supports 2 types of integration: +The Yahoo Advertising bid adapter supports 2 types of integration: 1. **dcn & pos** DEFAULT (Site/App & Position targeting) - For Display partners/publishers. -2. **pubId** (Publisher ID) - For legacy "oneVideo" AND New partners/publishers. -**Important:** pubId integration (option 2) is only possible when your Seller account is setup for "Inventory Mapping". +2. **pubId** (Publisher ID) - For legacy "oneVideo" AND new partners/publishers. +**Important:** pubId integration (option 2) is only possible when your seller account is setup for "Inventory Mapping". **Please Note:** Most examples in this file are using dcn & pos. ## Who is currently eligible for "pubId" integration At this time, only the following partners/publishers are eligble for pubId integration: -1. New partners/publishers that do not have any existing accounts on Yahoo SSP (aka: aol, oneMobile, oneDisplay). +1. New partners/publishers that do not have an existing account with Yahoo Advertising (aka: aol, oneMobile, oneDisplay). 2. Video SSP (oneVideo) partners/publishers that A. Do not have any display/banner inventory. - B. Do not have any existing accounts on Yahoo SSP (aka: aol, oneMobile, oneDisplay). + B. Do not have an existing account with Yahoo Advertising (aka: aol, oneMobile, oneDisplay). # Mandatory Bidder Parameters ## dcn & pos (DEFAULT) -The minimal requirements for the 'yahoossp' bid adapter to generate an outbound bid-request to our Yahoo SSP are: +The minimal requirements for the Yahoo Advertising bid adapter to generate an outbound bid-request to Yahoo's bid endpoint are: 1. At least 1 adUnit including mediaTypes: banner or video 2. **bidder.params** object must include: - A. **dcn:** Yahoo SSP Site/App inventory parameter. - B. **pos:** Yahoo SSP position inventory parameter. + A. **dcn:** Yahoo Advertising Site/App inventory parameter. + B. **pos:** Yahoo Advertising position inventory parameter. ### Example: dcn & pos Mandatory Parameters (Single banner adUnit) ```javascript @@ -76,10 +88,10 @@ const adUnits = [{ }, bids: [ { - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from SSP - pos: '8a969978017a7aaabab4ab0bc01a0009' // Placement ID provided from SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a969978017a7aaabab4ab0bc01a0009' // Placement ID provided by Yahoo Advertising } } ] @@ -87,10 +99,10 @@ const adUnits = [{ ``` ## pubId -The minimal requirements for the 'yahoossp' bid adapter to generate an outbound bid-request to our Yahoo SSP are: +The minimal requirements for the Yahoo Advertising bid adapter to generate an outbound bid-request to Yahoo's bid endpoint are: 1. At least 1 adUnit including mediaTypes: banner or video 2. **bidder.params** object must include: - A. **pubId:** Yahoo SSP Publisher ID (AKA oneVideo pubId/Exchange name) + A. **pubId:** Yahoo Advertising Publisher ID (AKA oneVideo pubId/Exchange name) ### Example: pubId Mandatory Parameters (Single banner adUnit) ```javascript @@ -103,14 +115,15 @@ const adUnits = [{ }, bids: [ { - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - pubId: 'DemoPublisher', // Publisher External ID provided from Yahoo SSP. + pubId: 'DemoPublisher', // Publisher defined external ID as configured by Yahoo Advertising. } } ] }]; ``` + # Advanced adUnit Examples: ## Banner ```javascript @@ -124,21 +137,22 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from Yahoo SSP - pos: '8a969978017a7aaabab4ab0bc01a0009', // Placement ID provided from Yahoo SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a969978017a7aaabab4ab0bc01a0009', // Placement ID provided by Yahoo Advertising } } }] }]; ``` + ## Video Instream -**Important!** Make sure that the Yahoo SSP Placement type (in-stream) matches the adUnit video inventory type. +**Important!** Make sure that the Yahoo Advertising Placement type (in-stream) matches the adUnit video inventory type. **Note:** Make sure to set the adapter mode to allow video requests by setting it to mode: 'video' OR mode: 'all'. ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'video' } }); @@ -156,20 +170,21 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from Yahoo SSP - pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided from Yahoo SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided by Yahoo Advertising } }] }]; + ``` ## Video Outstream -**Important!** Make sure that the Yahoo SSP Placement type (in-feed/ in-article) matches the adUnit video inventory type. +**Important!** Make sure that the Yahoo Advertsing placement type (in-feed/ in-article) matches the adUnit video inventory type. **Note:** Make sure to set the adapter mode to allow video requests by setting it to mode: 'video' OR mode: 'all' ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'video' } }); @@ -187,22 +202,22 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from Yahoo SSP - pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided from Yahoo SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided by Yahoo Advertising } }] }]; + ``` ## Multi-Format -**Important!** If you intend to use the yahoossp bidder for both Banner and Video formats please make sure: -1. Set the adapter as mode: 'all' - to call the Yahoo SSP for both banner & video formats. -2. Make sure the Yahoo SSP placement (pos id) supports both banner & video format requests. - +**Important!** If you intend to use the Yahoo Advertising bidder for both Banner and Video formats please make sure: +1. Set the adapter as mode: 'all' - to configure the bid adapter to call the bid endpoint for both banner & video formats. +2. Make sure the Yahoo Advertising placement (pos id) supports both banner & video format requests. ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'all' } }); @@ -223,19 +238,18 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from Yahoo SSP - pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided from Yahoo SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided by Yahoo Advertising } }] }]; ``` # Optional: Schain module support -The yahoossp adapter supports the Prebid.org Schain module and will pass it through to our Yahoo SSP +The Yahoo Advertising bid adapter supports the Prebid.org Schain module and will pass it through to our bid endpoint. For further details please see, https://docs.prebid.org/dev-docs/modules/schain.html - ## Global Schain Example: ```javascript pbjs.setConfig({ @@ -256,7 +270,7 @@ For further details please see, https://docs.prebid.org/dev-docs/modules/schain. ## Bidder Specific Schain Example: ```javascript pbjs.setBidderConfig({ - "bidders": ['yahoossp'], // can list more bidders here if they share the same config + "bidders": ['yahooAds'], // can list more bidders here if they share the same config "config": { "schain": { "validation": "strict", @@ -275,12 +289,10 @@ For further details please see, https://docs.prebid.org/dev-docs/modules/schain. ``` # Optional: Price floors module & bidfloor -The yahoossp adapter supports the Prebid.org Price Floors module and will use it to define the outbound bidfloor and currency. -By default the adapter will always check the existance of Module price floor. -If a module price floor does not exist you can set a custom bid floor for your impression using "params.bidOverride.imp.bidfloor". +The Yahoo Advertising bid adapter supports the Prebid.org Price Floors module and will use it to define the outbound bidfloor and currency, if the relevant floors have been defined in the configuration. +A cusom method for defining bid floors is also supported, this can be enabled by setting the `params.bidOverride.imp.bidfloor` bidder parameter. **Note:** All override params apply to all requests generated using this configuration regardless of format type. - ```javascript const adUnits = [{ code: 'override-pricefloor', @@ -292,10 +304,10 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from Yahoo SSP - pos: '8a969978017a7aaabab4ab0bc01a0009', // Placement ID provided from Yahoo SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a969978017a7aaabab4ab0bc01a0009', // Placement ID provided by Yahoo Advertising bidOverride :{ imp: { bidfloor: 5.00 // bidOverride priceFloor @@ -310,21 +322,21 @@ const adUnits = [{ For further details please see, https://docs.prebid.org/dev-docs/modules/floors.html # Optional: Self-served E2E testing mode -If you want to see how the yahoossp adapter works and loads you are invited to try it out using our testing mode. +If you want to see how the Yahoo Advertising bid adapter works and loads you are invited to try it out using our testing mode. This is useful for integration testing and response parsing when checking banner vs video capabilities. ## How to use E2E test mode: -1. Set the yahoossp global config mode to either 'banner' or 'video' - depending on the adUnit you want to test. +1. Set the `yahooAds` global config mode to either `'banner'` or `'video'` - depending on the adUnit you want to test. 2. Add params.testing.e2etest: true to your adUnit bidder config - See examples below. **Note:** When using E2E Test Mode you do not need to pass mandatory bidder params dcn or pos. -**Important!** E2E Testing Mode only works when the Bidder Request Mode is set explicitly to either 'banner' or 'video'. +**Important!** E2E Testing Mode only works when the Bidder Request Mode is set explicitly to either `'banner'` or `'video'`. ## Activating E2E Test for "Banner" ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'banner' // select 'banner' or 'video' to define what response to load } }); @@ -338,7 +350,7 @@ const adUnits = [{ }, bids: [ { - bidder: 'yahoossp', + bidder: 'yahooAds', params: { testing: { e2etest: true // Activate E2E Test mode @@ -353,7 +365,7 @@ const adUnits = [{ **Note:** We recommend using Video Outstream as it would load the video response using our Outstream Renderer feature ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'video' } }); @@ -371,7 +383,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { testing: { e2etest: true // Activate E2E Test mode @@ -382,8 +394,8 @@ const adUnits = [{ ``` # Optional: First Party Data -The yahoossp adapter now supports first party data passed via: -1. Global ortb2 object using pbjs.setConfig() +The Yahoo Advertising bid adapter supports first party data passed via: +1. Global ortb2 object using `pbjs.setConfig()` 2. adUnit ortb2Imp object declared within an adUnit. For further details please see, https://docs.prebid.org/features/firstPartyData.html ## Global First Party Data "ortb2" @@ -392,15 +404,15 @@ For further details please see, https://docs.prebid.org/features/firstPartyData. pbjs.setConfig({ ortb2: { site: { - name: 'yahooAdTech', - domain: 'yahooadtech.com', + name: 'Yahoo Advertising', + domain: 'yahooadvertising.com', cat: ['IAB2'], sectioncat: ['IAB2-2'], pagecat: ['IAB2-2'], - page: 'https://page.yahooadtech.com/here.html', - ref: 'https://ref.yahooadtech.com/there.html', + page: 'https://page.yahooadvertising.com.com/here.html', + ref: 'https://ref.yahooadvertising.com.com/there.html', keywords:'yahoo, ad, tech', - search: 'SSP', + search: 'header bidding', content: { id: '1234', title: 'Title', @@ -521,7 +533,7 @@ pbjs.setConfig({ ## AdUnit First Party Data "ortb2Imp" Most DSPs are adopting the Global Placement ID (GPID). -Please pass your placement specific GPID value to Yahoo SSP using `adUnit.ortb2Imp.ext.data.pbadslot`. +Please pass your placement specific GPID value by setting `adUnit.ortb2Imp.ext.data.pbadslot`. ```javascript const adUnits = [{ code: 'placement', @@ -541,7 +553,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { pubdId: 'DemoPublisher' } @@ -551,7 +563,7 @@ const adUnits = [{ ``` # Optional: Bidder bidOverride Parameters -The yahoossp adapter allows passing override data to the outbound bid-request in that overrides First Party Data. +The Yahoo Advertising bid adapter allows passing override data to the outbound bid-request that overrides First Party Data. **Important!** We highly recommend using prebid modules to pass data instead of bidder speicifc overrides. The use of these parameters are a last resort to force a specific feature or use case in your implementation. @@ -577,7 +589,6 @@ Currently the bidOverride object only accepts the following: * device * ip - ```javascript const adUnits = [{ code: 'bidOverride-adUnit', @@ -590,7 +601,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { dcn: '8a969516017a7a396ec539d97f540011', pos: '8a96958a017a7a57ac375d50c0c700cc', @@ -615,7 +626,7 @@ const adUnits = [{ } }, site: { - page: 'https://yahoossp-bid-adapter.com', + page: 'https://yahooAdvertising-bid-adapter.com', }, device: { ip: "1.2.3.4" @@ -630,7 +641,7 @@ const adUnits = [{ Custom key-value paris can be used for both inventory targeting and reporting. You must set up key-value pairs in the Yahoo SSP before sending them via the adapter otherwise the Ad Server will not be listening and picking them up. -Important! Key-value pairs can only contain values of types: String, Number, Array of strings OR Array of numbers +Important! Key-value pairs can only contain values of the following data types: String, Number, Array of strings OR Array of numbers ```javascript const adUnits = [{ @@ -644,7 +655,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { dcn: '8a969516017a7a396ec539d97f540011', pos: '8a96958a017a7a57ac375d50c0c700cc', @@ -660,14 +671,14 @@ const adUnits = [{ ``` # Optional: Custom Cache Time To Live (ttl): -The yahoossp adapter supports passing of "Time To Live" (ttl) that indicates to prebid chache for how long to keep the chaced winning bid alive. Value is Number in seconds and you can enter any number between 1 - 3600 (seconds). -The setting can be defined globally using setConfig or within the adUnit.params. -Global level setConfig overrides adUnit.params. +The Yahoo Advertising bid adapter supports passing of "Time To Live" (ttl) to indicate to prebid how long the bid response from Yahoo Advertising should be retained by Prebid for. This configuration value must be a Number in seconds, with the valid range being 1 - 3600 inclusive. +The setting can be defined globally using `setConfig` or within the adUnit.params. +Global level `setConfig` overrides adUnit.params. If no value is being passed default is 300 seconds. ## Global TTL ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { ttl: 300 } }); @@ -683,7 +694,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { dcn: '8a969516017a7a396ec539d97f540011', pos: '8a96958a017a7a57ac375d50c0c700cc', @@ -702,7 +713,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { dcn: '8a969516017a7a396ec539d97f540011', pos: '8a96958a017a7a57ac375d50c0c700cc', @@ -710,10 +721,11 @@ const adUnits = [{ } }] }] + ``` # Optional: Video Features ## Rewarded video flag -To indicate to Yahoo SSP that this adUnit is a rewarded video you can pass the following in the params.bidOverride.imp.video.rewarded: 1 +To indicate to Yahoo Advertising that this adUnit is a rewarded video you can set the `params.bidOverride.imp.video.rewarded` property to `1` ```javascript const adUnits = [{ @@ -727,7 +739,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { dcn: '8a969516017a7a396ec539d97f540011', pos: '8a96958a017a7a57ac375d50c0c700cc', @@ -744,7 +756,7 @@ const adUnits = [{ ``` ## Site/App Targeting for "pubId" Inventory Mapping -To target your adUnit explicitly to a specific Site/App Object in Yahoo SSP, you can pass one of the following: +To target your adUnit explicitly to a specific Site/App Object in Yahoo Advertising, you can pass one of the following: 1. params.siteId = External Site ID || Video SSP RTBIS Id (in String format). 2. params.bidOverride.site.id = External Site ID || Video SSP RTBIS Id (in String format). **Important:** Site override is a only supported when using "pubId" mode. @@ -762,7 +774,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { pubId: 'DemoPublisher', siteId: '1234567'; @@ -772,13 +784,13 @@ const adUnits = [{ ``` ## Placement Targeting for "pubId" Inventory Mapping -To target your adUnit explicitly to a specific Placement within a Site/App Object in Yahoo SSP, you can pass the following params.placementId = External Placement ID || Placement Alias +To target your adUnit explicitly to a specific Placement within a Site/App Object in Yahoo Advertising, you can pass the following params.placementId = External Placement ID || Placement Alias **Important!** Placement override is a only supported when using "pubId" mode. -**Important!** It is highly recommended that you pass both `siteId` AND `placementId` together to avoid inventory miss matching. +**Important!** It is highly recommended that you pass both `siteId` AND `placementId` together to avoid inventory mismatching. ### Site & Placement override -**Important!** If the placement ID does not reside under the defined Site/App object, the request will not resolve and no response will be sent back from the ad-server. +**Important!** If the placement ID does not reside under the defined Site/App object, the request will not resolve and no response will be sent back from the bid-server. ```javascript const adUnits = [{ code: 'pubId-site-targeting-adUnit', @@ -791,7 +803,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { pubId: 'DemoPublisher', siteId: '1234567', @@ -801,7 +813,7 @@ const adUnits = [{ }] ``` ### Placement only override -**Important!** Using this method is not advised if you have multiple Site/Apps that are broken out of a Run Of Network (RON) Site/App. If the placement ID does not reside under a matching Site/App object, the request will not resolve and no response will be sent back from the ad-server. +**Important!** Using this method is not advised if you have multiple Site/Apps that are broken out of a Run Of Network (RON) Site/App. If the placement ID does not reside under a matching Site/App object, the request will not resolve and no response will be sent back from the bid-server. ```javascript const adUnits = [{ code: 'pubId-site-targeting-adUnit', @@ -814,7 +826,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { pubId: 'DemoPublisher', placementId: 'header-250x300' @@ -824,9 +836,8 @@ const adUnits = [{ ``` # Optional: Legacy override Parameters -This adapter does not support passing legacy overrides via 'bidder.params.ext' since most of the data should be passed using prebid modules (First Party Data, Schain, Price Floors etc.). +This adapter does not support passing legacy overrides via `bidder.params.ext` since most of the data should be passed using prebid modules (First Party Data, Schain, Price Floors etc.). If you do not know how to pass a custom parameter that you previously used, please contact us using the information provided above. Thank you, -Yahoo SSP - +Yahoo Advertsing diff --git a/modules/yandexBidAdapter.js b/modules/yandexBidAdapter.js index 2870229f1db..9ca989b2259 100644 --- a/modules/yandexBidAdapter.js +++ b/modules/yandexBidAdapter.js @@ -1,7 +1,8 @@ -import { formatQS, deepAccess, triggerPixel, _each, _map } from '../src/utils.js'; +import { formatQS, deepAccess, deepSetValue, triggerPixel, _each, _map } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js' import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'yandex'; const BIDDER_URL = 'https://bs.yandex.ru/prebid'; @@ -76,9 +77,11 @@ export const spec = { timeout = bidderRequest.timeout; } + const adServerCurrency = config.getConfig('currency.adServerCurrency'); + return validBidRequests.map((bidRequest) => { const { params } = bidRequest; - const { targetRef, withCredentials = true } = params; + const { targetRef, withCredentials = true, cur } = params; const { pageId, impId } = extractPlacementIds(params); @@ -107,20 +110,32 @@ export const spec = { imp.bidfloorcur = bidfloor.currency; } + const currency = cur || adServerCurrency; + if (currency) { + queryParams['ssp-cur'] = currency; + } + + const data = { + id: bidRequest.bidId, + imp: [imp], + site: { + ref: referrer, + page, + domain, + }, + tmax: timeout, + }; + + const eids = deepAccess(bidRequest, 'userIdAsEids'); + if (eids && eids.length) { + deepSetValue(data, 'user.ext.eids', eids); + } + const queryParamsString = formatQS(queryParams); return { method: 'POST', url: BIDDER_URL + `/${pageId}?${queryParamsString}`, - data: { - id: bidRequest.bidId, - imp: [imp], - site: { - ref: referrer, - page, - domain, - }, - tmax: timeout, - }, + data, options: { withCredentials, }, @@ -132,24 +147,13 @@ export const spec = { interpretResponse: interpretResponse, onBidWon: function (bid) { - let nurl = bid['nurl']; + const nurl = bid['nurl']; if (!nurl) { return; } - let cpm, currency; - if (bid.hasOwnProperty('originalCurrency') && bid.hasOwnProperty('originalCpm')) { - cpm = bid.originalCpm; - currency = bid.originalCurrency; - } else { - cpm = bid.cpm; - currency = bid.currency; - } - cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || cpm; - - const pixel = replaceAuctionPrice(nurl, cpm, currency); - triggerPixel(pixel); + triggerPixel(nurl); } } @@ -316,7 +320,7 @@ function interpretResponse(serverResponse, { bidRequest }) { width: bidReceived.w, height: bidReceived.h, creativeId: bidReceived.adid, - nurl: bidReceived.nurl, + nurl: replaceAuctionPrice(bidReceived.nurl, price, currency), netRevenue: true, ttl: DEFAULT_TTL, diff --git a/modules/yieldloveBidAdapter.js b/modules/yieldloveBidAdapter.js new file mode 100644 index 00000000000..4568206b20a --- /dev/null +++ b/modules/yieldloveBidAdapter.js @@ -0,0 +1,149 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const ENDPOINT_URL = 'https://s2s.yieldlove-ad-serving.net/openrtb2/auction'; + +const DEFAULT_BID_TTL = 300; /* 5 minutes */ +const DEFAULT_CURRENCY = 'EUR'; + +const participatedBidders = [] + +export const spec = { + gvlid: 251, + code: 'yieldlove', + aliases: [], + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!(bid.params.pid && bid.params.rid) + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const anyValidBidRequest = validBidRequests[0] + + const impressions = validBidRequests.map(bidRequest => { + return { + ext: { + prebid: { + storedrequest: { + id: bidRequest.params.pid?.toString() + } + } + }, + banner: { + format: bidRequest.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1], + })) + }, + secure: 1, + id: bidRequest.bidId + } + }) + + const s2sRequest = { + device: { + ua: window.navigator.userAgent, + w: window.innerWidth, + h: window.innerHeight, + }, + site: { + ver: '1.9.0', + publisher: { + id: anyValidBidRequest.params.rid + }, + page: window.location.href, + domain: anyValidBidRequest.params.rid + }, + ext: { + prebid: { + targeting: {}, + cache: { + bids: {} + }, + storedrequest: { + id: anyValidBidRequest.params.rid + }, + } + }, + user: { + ext: { + consent: bidderRequest.gdprConsent?.consentString + }, + }, + id: utils.generateUUID(), + imp: impressions, + regs: { + ext: { + gdpr: 1 + } + } + } + + return { + method: 'POST', + url: ENDPOINT_URL, + data: s2sRequest, + options: { + contentType: 'text/plain', + withCredentials: true + }, + }; + }, + + interpretResponse: function (serverResponse) { + const bidResponses = [] + const seatBids = serverResponse.body?.seatbid || [] + seatBids.reduce((bids, cur) => { + if (cur.bid && cur.bid.length > 0) bids = bids.concat(cur.bid) + return bids + }, []).forEach(bid => { + bidResponses.push({ + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + ad: bid.adm, + ttl: DEFAULT_BID_TTL, + creativeId: bid.crid, + netRevenue: true, + currency: DEFAULT_CURRENCY + }) + }) + + const bidders = serverResponse.body?.ext.responsetimemillis || {} + Object.keys(bidders).forEach(bidder => { + if (!participatedBidders.includes(bidder)) participatedBidders.push(bidder) + }) + + if (bidResponses.length === 0) { + utils.logInfo('interpretResponse :: no bid'); + } + + return bidResponses; + }, + + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + let gdprParams = '' + gdprParams = `gdpr=${Number(gdprConsent?.gdprApplies)}&` + gdprParams += `gdpr_consent=${gdprConsent?.consentString || ''}` + + let bidderParams = '' + if (participatedBidders.length > 0) { + bidderParams = `bidders=${participatedBidders.join(',')}` + } + + syncs.push({ + type: 'iframe', + url: `https://cdn-a.yieldlove.com/load-cookie.html?endpoint=yieldlove&max_sync_count=100&${gdprParams}&${bidderParams}` + }) + + return syncs + }, + +}; + +registerBidder(spec); diff --git a/modules/yieldloveBidAdapter.md b/modules/yieldloveBidAdapter.md new file mode 100644 index 00000000000..8fd71b55f88 --- /dev/null +++ b/modules/yieldloveBidAdapter.md @@ -0,0 +1,38 @@ +# Overview + +``` +Module Name: Yieldlove Bid Adapter +Module Type: Bidder Adapter +Maintainer: adapter@yieldlove.com +``` + + +# Description + +Connects to **[Yieldlove](https://www.yieldlove.com/)**s S2S platform for bids. + +```js +const adUnits = [ + { + code: 'test-div', + mediaTypes: { banner: { sizes: [[300, 250]] }}, + bids: [ + { + bidder: 'yieldlove', + params: { + pid: 34437, + rid: 'website.com' + } + } + ] + } +] +``` + + +# Bid Parameters + +| Name | Scope | Description | Example | Type | +|---------------|--------------|---------------------------------------------------------|----------------------------|--------------| +| rid | **required** | Publisher ID on the Yieldlove platform | `website.com` | String | +| pid | **required** | Placement ID on the Yieldlove platform | `34437` | Number | diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 40276aa7a6c..e72a671ac74 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -78,14 +78,17 @@ export const spec = { bust: new Date().getTime().toString(), dnt: getDNT(), description: getPageDescription(), + tmax: bidderRequest.timeout || 400, userConsent: JSON.stringify({ // case of undefined, stringify will remove param - gdprApplies: deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', + gdprApplies: + deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', cmp: deepAccess(bidderRequest, 'gdprConsent.consentString') || '', gpp: deepAccess(bidderRequest, 'gppConsent.gppString') || '', - gpp_sid: deepAccess(bidderRequest, 'gppConsent.applicableSections') || [] + gpp_sid: + deepAccess(bidderRequest, 'gppConsent.applicableSections') || [], }), - us_privacy: deepAccess(bidderRequest, 'uspConsent') || '' + us_privacy: deepAccess(bidderRequest, 'uspConsent') || '', }; if (canAccessTopWindow()) { @@ -432,12 +435,12 @@ function openRtbImpression(bidRequest) { } }; - const mediaTypesParams = deepAccess(bidRequest, 'mediaTypes.video'); + const mediaTypesParams = deepAccess(bidRequest, 'mediaTypes.video', {}); Object.keys(mediaTypesParams) .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) .forEach(param => imp.video[param] = mediaTypesParams[param]); - const videoParams = deepAccess(bidRequest, 'params.video'); + const videoParams = deepAccess(bidRequest, 'params.video', {}); Object.keys(videoParams) .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) .forEach(param => imp.video[param] = videoParams[param]); @@ -607,8 +610,14 @@ function validateVideoParams(bid) { } validate('video.protocols', val => isDefined(val), paramRequired); - validate('video.protocols', val => isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), - paramInvalid, 'array of numbers, ex: [2,3]'); + validate( + 'video.protocols', + (val) => + isArrayOfNums(val) && + val.every((v) => v >= 1 && v <= 12 && v != 9 && v != 10), // 9 and 10 are for DAST which are not supported. + paramInvalid, + 'array of numbers between 1 and 12 except for 9 or 10 , ex: [2,3, 7, 11]' + ); validate('video.api', val => isDefined(val), paramRequired); validate('video.api', val => isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), diff --git a/modules/zeotapIdPlusIdSystem.js b/modules/zeotapIdPlusIdSystem.js index 4cb827cbdff..ab7bf7c237b 100644 --- a/modules/zeotapIdPlusIdSystem.js +++ b/modules/zeotapIdPlusIdSystem.js @@ -60,6 +60,12 @@ export const zeotapIdPlusSubmodule = { getId() { const id = readCookie() || readFromLocalStorage(); return id ? { id } : undefined; + }, + eids: { + 'IDP': { + source: 'zeotap.com', + atype: 1 + }, } }; submodule('userId', zeotapIdPlusSubmodule); diff --git a/modules/zeta_global_sspAnalyticsAdapter.js b/modules/zeta_global_sspAnalyticsAdapter.js index ee3bb9cd5d6..751b55ec673 100644 --- a/modules/zeta_global_sspAnalyticsAdapter.js +++ b/modules/zeta_global_sspAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { logInfo, logError } from '../src/utils.js'; +import {logInfo, logError} from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; @@ -10,6 +10,10 @@ const ADAPTER_CODE = 'zeta_global_ssp'; const BASE_URL = 'https://ssp.disqus.com/prebid/event'; const LOG_PREFIX = 'ZetaGlobalSsp-Analytics: '; +const cache = { + auctions: {} +}; + /// /////////// VARIABLES //////////////////////////////////// let publisherId; // int @@ -24,20 +28,87 @@ function sendEvent(eventType, event) { ); } +function getZetaParams(event) { + if (event.adUnits) { + for (const i in event.adUnits) { + const unit = event.adUnits[i]; + if (unit.bids) { + for (const j in unit.bids) { + const bid = unit.bids[j]; + if (bid.bidder === ADAPTER_CODE && bid.params) { + return bid.params; + } + } + } + } + } + return null; +} + /// /////////// ADAPTER EVENT HANDLER FUNCTIONS ////////////// function adRenderSucceededHandler(args) { let eventType = CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED logInfo(LOG_PREFIX + 'handle ' + eventType + ' event'); - sendEvent(eventType, args); + const event = { + adId: args.adId, + bid: { + adId: args.bid?.adId, + auctionId: args.bid?.auctionId, + adUnitCode: args.bid?.adUnitCode, + bidId: args.bid?.bidId, + requestId: args.bid?.requestId, + bidderCode: args.bid?.bidderCode, + mediaTypes: args.bid?.mediaTypes, + sizes: args.bid?.sizes, + adserverTargeting: args.bid?.adserverTargeting, + cpm: args.bid?.cpm, + creativeId: args.bid?.creativeId, + mediaType: args.bid?.mediaType, + renderer: args.bid?.renderer, + size: args.bid?.size, + timeToRespond: args.bid?.timeToRespond, + params: args.bid?.params + }, + doc: { + location: args.doc?.location + } + } + + // set zetaParams from cache + if (event.bid && event.bid.auctionId) { + const zetaParams = cache.auctions[event.bid.auctionId]; + if (zetaParams) { + event.bid.params = [ zetaParams ]; + } + } + + sendEvent(eventType, event); } function auctionEndHandler(args) { let eventType = CONSTANTS.EVENTS.AUCTION_END; logInfo(LOG_PREFIX + 'handle ' + eventType + ' event'); - sendEvent(eventType, args); + const event = { + adUnitCodes: args.adUnitCodes, + adUnits: args.adUnits, + auctionEnd: args.auctionEnd, + auctionId: args.auctionId, + bidderRequests: args.bidderRequests, + bidsReceived: args.bidsReceived, + noBids: args.noBids, + winningBids: args.winningBids + } + + // save zetaParams to cache + const zetaParams = getZetaParams(event); + if (zetaParams && event.auctionId) { + cache.auctions[event.auctionId] = zetaParams; + } + + sendEvent(eventType, event); } /// /////////// ADAPTER DEFINITION /////////////////////////// diff --git a/modules/zeta_global_sspBidAdapter.js b/modules/zeta_global_sspBidAdapter.js index 687afb6c692..db31791d86f 100644 --- a/modules/zeta_global_sspBidAdapter.js +++ b/modules/zeta_global_sspBidAdapter.js @@ -45,7 +45,7 @@ export const spec = { supportedMediaTypes: [BANNER, VIDEO], /** - * Determines whether or not the given bid request is valid. + * Determines whether the given bid request is valid. * * @param {BidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. @@ -54,7 +54,8 @@ export const spec = { // check for all required bid fields if (!(bid && bid.bidId && - bid.params)) { + bid.params && + bid.params.sid)) { logWarn('Invalid bid request - missing required bid data'); return false; } @@ -162,7 +163,7 @@ export const spec = { } provideEids(validBidRequests[0], payload); - const url = params.shortname ? ENDPOINT_URL.concat('?shortname=', params.shortname) : ENDPOINT_URL; + const url = params.sid ? ENDPOINT_URL.concat('?sid=', params.sid) : ENDPOINT_URL; return { method: 'POST', url: url, diff --git a/package-lock.json b/package-lock.json index af7eda1ea6b..caee07c67f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "8.1.0-pre", + "version": "8.25.0-pre", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "7.51.0-pre", + "version": "8.24.0-pre", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.16.7", @@ -16,13 +16,13 @@ "core-js": "^3.13.0", "core-js-pure": "^3.13.0", "criteo-direct-rsa-validate": "^1.1.0", - "crypto-js": "^3.3.0", + "crypto-js": "^4.2.0", "dlv": "1.1.3", "dset": "3.1.2", "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "^5.0.0" + "live-connect-js": "^6.3.0" }, "devDependencies": { "@babel/eslint-parser": "^7.16.5", @@ -127,11 +127,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -193,12 +194,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz", - "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.20.0", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -310,9 +312,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } @@ -329,23 +331,23 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -465,28 +467,28 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -527,12 +529,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -540,9 +542,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz", - "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1612,31 +1614,31 @@ } }, "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1645,12 +1647,12 @@ } }, "node_modules/@babel/types": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz", - "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -6847,9 +6849,9 @@ } }, "node_modules/cac/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -7838,9 +7840,9 @@ } }, "node_modules/crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "node_modules/css": { "version": "3.0.0", @@ -9653,9 +9655,9 @@ } }, "node_modules/eslint/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -9903,9 +9905,9 @@ } }, "node_modules/execa/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -12090,9 +12092,9 @@ } }, "node_modules/gulp-cli/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -12432,9 +12434,9 @@ } }, "node_modules/gulp-eslint/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -15796,9 +15798,9 @@ } }, "node_modules/karma-coverage-istanbul-reporter/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -16232,23 +16234,23 @@ "dev": true }, "node_modules/live-connect-common": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-1.0.0.tgz", - "integrity": "sha512-LBZsvykcGeVRYI1eqqXrrNZsoBdL2a8cpyrYPIiGAF/CpixbyRbvqGslaFw511lH294QB16J3fYYg21aYuaM2Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.2.tgz", + "integrity": "sha512-K3LNKd9CpREDJbXGdwKqPojjQaxd4G6c7OAD6Yzp3wsCWTH2hV8xNAbUksSOpOcVyyOT9ilteEFXIJQJrbODxQ==", "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/live-connect-js": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-5.0.0.tgz", - "integrity": "sha512-Bv0wQQ+/1VU0/YczEpObbWtHbuXwaHGxwg1+Pe7ZlDgBLb334CrqSQvOL1uyZw3//zs+fSO94yYaQzjjkTd5OQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.3.0.tgz", + "integrity": "sha512-1SnXQZq9gxHIb0scXPX1Da1rQ0oY2sloMGgeRreTAwhCtdQEuip/IYwgOh3/ZeZ6yT6iG9FLb7+AjORC4pO46g==", "dependencies": { - "live-connect-common": "^1.0.0", + "live-connect-common": "^v3.0.2", "tiny-hashes": "1.0.1" }, "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/livereload-js": { @@ -18739,9 +18741,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -21034,9 +21036,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -24904,9 +24906,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -25289,11 +25291,12 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -25335,12 +25338,13 @@ } }, "@babel/generator": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz", - "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "requires": { - "@babel/types": "^7.20.0", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { @@ -25421,9 +25425,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" }, "@babel/helper-explode-assignable-expression": { "version": "7.18.6", @@ -25434,20 +25438,20 @@ } }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -25534,22 +25538,22 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -25578,19 +25582,19 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz", - "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==" + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.18.6", @@ -26282,39 +26286,39 @@ } }, "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz", - "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -30515,9 +30519,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "strip-ansi": { @@ -31278,9 +31282,9 @@ } }, "crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "css": { "version": "3.0.0", @@ -32530,9 +32534,9 @@ "dev": true }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -32900,9 +32904,9 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "shebang-command": { @@ -34681,9 +34685,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "string-width": { @@ -34969,9 +34973,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -37585,9 +37589,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "source-map": { @@ -37883,16 +37887,16 @@ "dev": true }, "live-connect-common": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-1.0.0.tgz", - "integrity": "sha512-LBZsvykcGeVRYI1eqqXrrNZsoBdL2a8cpyrYPIiGAF/CpixbyRbvqGslaFw511lH294QB16J3fYYg21aYuaM2Q==" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.2.tgz", + "integrity": "sha512-K3LNKd9CpREDJbXGdwKqPojjQaxd4G6c7OAD6Yzp3wsCWTH2hV8xNAbUksSOpOcVyyOT9ilteEFXIJQJrbODxQ==" }, "live-connect-js": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-5.0.0.tgz", - "integrity": "sha512-Bv0wQQ+/1VU0/YczEpObbWtHbuXwaHGxwg1+Pe7ZlDgBLb334CrqSQvOL1uyZw3//zs+fSO94yYaQzjjkTd5OQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.3.0.tgz", + "integrity": "sha512-1SnXQZq9gxHIb0scXPX1Da1rQ0oY2sloMGgeRreTAwhCtdQEuip/IYwgOh3/ZeZ6yT6iG9FLb7+AjORC4pO46g==", "requires": { - "live-connect-common": "^1.0.0", + "live-connect-common": "^v3.0.2", "tiny-hashes": "1.0.1" } }, @@ -39761,9 +39765,9 @@ }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -41495,9 +41499,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" }, "semver-greatest-satisfied-range": { "version": "1.1.0", @@ -44558,9 +44562,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true }, "wordwrap": { diff --git a/package.json b/package.json index 4f0f512bd8b..7076d012285 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.1.0-pre", + "version": "8.25.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { @@ -126,13 +126,13 @@ "core-js": "^3.13.0", "core-js-pure": "^3.13.0", "criteo-direct-rsa-validate": "^1.1.0", - "crypto-js": "^3.3.0", + "crypto-js": "^4.2.0", "dlv": "1.1.3", "dset": "3.1.2", "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "^5.0.0" + "live-connect-js": "^6.3.0" }, "optionalDependencies": { "fsevents": "^2.3.2" diff --git a/src/activities/params.js b/src/activities/params.js index ff181bb55a4..036a6657cf8 100644 --- a/src/activities/params.js +++ b/src/activities/params.js @@ -1,4 +1,5 @@ import {MODULE_TYPE_BIDDER} from './modules.js'; +import {hook} from '../hook.js'; /** * Component ID - who is trying to perform the activity? @@ -54,6 +55,8 @@ export function activityParamsBuilder(resolveAlias) { if (moduleType === MODULE_TYPE_BIDDER) { defaults[ACTIVITY_PARAM_ADAPTER_CODE] = resolveAlias(moduleName); } - return Object.assign(defaults, params); + return buildActivityParams(Object.assign(defaults, params)); } } + +export const buildActivityParams = hook('sync', params => params); diff --git a/src/activities/redactor.js b/src/activities/redactor.js index d50df72648c..d052c029c13 100644 --- a/src/activities/redactor.js +++ b/src/activities/redactor.js @@ -8,7 +8,17 @@ import { ACTIVITY_TRANSMIT_UFPD } from './activities.js'; -export const ORTB_UFPD_PATHS = ['user.data', 'user.ext.data']; +export const ORTB_UFPD_PATHS = [ + 'data', + 'ext.data', + 'yob', + 'gender', + 'keywords', + 'kwarray', + 'id', + 'buyeruid', + 'customdata' +].map(f => `user.${f}`); export const ORTB_EIDS_PATHS = ['user.eids', 'user.ext.eids']; export const ORTB_GEO_PATHS = ['user.geo.lat', 'user.geo.lon', 'device.geo.lat', 'device.geo.lon']; diff --git a/src/adapterManager.js b/src/adapterManager.js index b600802b522..575d28b35fa 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -1,8 +1,6 @@ /** @module adaptermanger */ import { - _each, - bind, deepAccess, deepClone, flatten, @@ -12,7 +10,8 @@ import { getUniqueIdentifierStr, getUserConfiguredParams, groupBy, - isArray, isPlainObject, + isArray, + isPlainObject, isValidMediaTypes, logError, logInfo, @@ -30,7 +29,7 @@ import {hook} from './hook.js'; import {find, includes} from './polyfill.js'; import {adunitCounter} from './adUnits.js'; import {getRefererInfo} from './refererDetection.js'; -import {GDPR_GVLIDS, GdprConsentHandler, GppConsentHandler, UspConsentHandler} from './consentHandler.js'; +import {GDPR_GVLIDS, gdprDataHandler, gppDataHandler, uspDataHandler, } from './consentHandler.js'; import * as events from './events.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; @@ -41,6 +40,8 @@ import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from './activities/activ import {ACTIVITY_PARAM_ANL_CONFIG, ACTIVITY_PARAM_S2S_NAME, activityParamsBuilder} from './activities/params.js'; import {redactor} from './activities/redactor.js'; +export {gdprDataHandler, gppDataHandler, uspDataHandler, coppaDataHandler} from './consentHandler.js'; + export const PBS_ADAPTER_NAME = 'pbsBidAdapter'; export const PARTITIONS = { CLIENT: 'client', @@ -192,16 +193,6 @@ function getAdUnitCopyForClientAdapters(adUnits) { return adUnitsClientCopy; } -export let gdprDataHandler = new GdprConsentHandler(); -export let uspDataHandler = new UspConsentHandler(); -export let gppDataHandler = new GppConsentHandler(); - -export let coppaDataHandler = { - getCoppa: function() { - return !!(config.getConfig('coppa')) - } -}; - /** * Filter and/or modify media types for ad units based on the given labels. * @@ -382,13 +373,13 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request return; } - let [clientBidRequests, serverBidRequests] = bidRequests.reduce((partitions, bidRequest) => { + let [clientBidderRequests, serverBidderRequests] = bidRequests.reduce((partitions, bidRequest) => { partitions[Number(typeof bidRequest.src !== 'undefined' && bidRequest.src === CONSTANTS.S2S.SRC)].push(bidRequest); return partitions; }, [[], []]); var uniqueServerBidRequests = []; - serverBidRequests.forEach(serverBidRequest => { + serverBidderRequests.forEach(serverBidRequest => { var index = -1; for (var i = 0; i < uniqueServerBidRequests.length; ++i) { if (serverBidRequest.uniquePbsTid === uniqueServerBidRequests[i].uniquePbsTid) { @@ -415,14 +406,17 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request let uniquePbsTid = uniqueServerBidRequests[counter].uniquePbsTid; let adUnitsS2SCopy = uniqueServerBidRequests[counter].adUnitsS2SCopy; - let uniqueServerRequests = serverBidRequests.filter(serverBidRequest => serverBidRequest.uniquePbsTid === uniquePbsTid); + let uniqueServerRequests = serverBidderRequests.filter(serverBidRequest => serverBidRequest.uniquePbsTid === uniquePbsTid); if (s2sAdapter) { let s2sBidRequest = {'ad_units': adUnitsS2SCopy, s2sConfig, ortb2Fragments}; if (s2sBidRequest.ad_units.length) { let doneCbs = uniqueServerRequests.map(bidRequest => { bidRequest.start = timestamp(); - return doneCb.bind(bidRequest); + return function () { + onTimelyResponse(bidRequest.bidderRequestId); + doneCb.apply(bidRequest, arguments); + } }); const bidders = getBidderCodes(s2sBidRequest.ad_units).filter((bidder) => adaptersServerSide.includes(bidder)); @@ -437,7 +431,7 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request // make bid requests s2sAdapter.callBids( s2sBidRequest, - serverBidRequests, + serverBidderRequests, addBidResponse, () => doneCbs.forEach(done => done()), s2sAjax @@ -451,35 +445,34 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request }); // handle client adapter requests - clientBidRequests.forEach(bidRequest => { - bidRequest.start = timestamp(); + clientBidderRequests.forEach(bidderRequest => { + bidderRequest.start = timestamp(); // TODO : Do we check for bid in pool from here and skip calling adapter again ? - const adapter = _bidderRegistry[bidRequest.bidderCode]; - config.runWithBidder(bidRequest.bidderCode, () => { + const adapter = _bidderRegistry[bidderRequest.bidderCode]; + config.runWithBidder(bidderRequest.bidderCode, () => { logMessage(`CALLING BIDDER`); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidderRequest); }); let ajax = ajaxBuilder(requestBidsTimeout, requestCallbacks ? { - request: requestCallbacks.request.bind(null, bidRequest.bidderCode), + request: requestCallbacks.request.bind(null, bidderRequest.bidderCode), done: requestCallbacks.done } : undefined); - const adapterDone = doneCb.bind(bidRequest); + const adapterDone = doneCb.bind(bidderRequest); try { config.runWithBidder( - bidRequest.bidderCode, - bind.call( - adapter.callBids, + bidderRequest.bidderCode, + adapter.callBids.bind( adapter, - bidRequest, + bidderRequest, addBidResponse, adapterDone, ajax, - onTimelyResponse, - config.callbackWithBidder(bidRequest.bidderCode) + () => onTimelyResponse(bidderRequest.bidderRequestId), + config.callbackWithBidder(bidderRequest.bidderCode) ) ); } catch (e) { - logError(`${bidRequest.bidderCode} Bid Adapter emitted an uncaught error when parsing their bidRequest`, {e, bidRequest}); + logError(`${bidderRequest.bidderCode} Bid Adapter emitted an uncaught error when parsing their bidRequest`, {e, bidRequest: bidderRequest}); adapterDone(); } }); @@ -547,6 +540,9 @@ adapterManager.aliasBidAdapter = function (bidderCode, alias, options) { } else { let spec = bidAdapter.getSpec(); let gvlid = options && options.gvlid; + if (spec.gvlid != null && gvlid == null) { + logWarn(`Alias '${alias}' will NOT re-use the GVL ID of the original adapter ('${spec.code}', gvlid: ${spec.gvlid}). Functionality that requires TCF consent may not work as expected.`) + } let skipPbsAliasing = options && options.skipPbsAliasing; newAdapter = newBidder(Object.assign({}, spec, { code: alias, gvlid, skipPbsAliasing })); _aliasRegistry[alias] = bidderCode; @@ -593,7 +589,7 @@ adapterManager.enableAnalytics = function (config) { config = [config]; } - _each(config, adapterConfig => { + config.forEach(adapterConfig => { const entry = _analyticsRegistry[adapterConfig.provider]; if (entry && entry.adapter) { if (dep.isAllowed(ACTIVITY_REPORT_ANALYTICS, activityParams(MODULE_TYPE_ANALYTICS, adapterConfig.provider, {[ACTIVITY_PARAM_ANL_CONFIG]: adapterConfig}))) { diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index c8f03bcfc94..8628f2c60a0 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -15,7 +15,7 @@ import { logError, logWarn, memoize, parseQueryStringParameters, - parseSizesInput, + parseSizesInput, pick, uniques } from '../utils.js'; import {hook} from '../hook.js'; @@ -25,7 +25,7 @@ import {useMetrics} from '../utils/perfMetrics.js'; import {isActivityAllowed} from '../activities/rules.js'; import {activityParams} from '../activities/activityParams.js'; import {MODULE_TYPE_BIDDER} from '../activities/modules.js'; -import {ACTIVITY_TRANSMIT_TID} from '../activities/activities.js'; +import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../activities/activities.js'; /** * This file aims to support Adapters during the Prebid 0.x -> 1.x transition. @@ -150,6 +150,7 @@ import {ACTIVITY_TRANSMIT_TID} from '../activities/activities.js'; // common params for all mediaTypes const COMMON_BID_RESPONSE_KEYS = ['cpm', 'ttl', 'creativeId', 'netRevenue', 'currency']; +const TIDS = ['auctionId', 'transactionId']; /** * Register a bidder with prebid, using the given spec. @@ -184,7 +185,7 @@ export function registerBidder(spec) { } } -function guardTids(bidderCode) { +export function guardTids(bidderCode) { if (isActivityAllowed(ACTIVITY_TRANSMIT_TID, activityParams(MODULE_TYPE_BIDDER, bidderCode))) { return { bidRequest: (br) => br, @@ -192,12 +193,20 @@ function guardTids(bidderCode) { }; } function get(target, prop, receiver) { - if (['transactionId', 'auctionId'].includes(prop)) { + if (TIDS.includes(prop)) { return null; } return Reflect.get(target, prop, receiver); } - const bidRequest = memoize((br) => new Proxy(br, {get}), (arg) => arg.bidId) + function privateAccessProxy(target, handler) { + const proxy = new Proxy(target, handler); + // always allow methods (such as getFloor) private access to TIDs + Object.entries(target) + .filter(([_, v]) => typeof v === 'function') + .forEach(([prop, fn]) => proxy[prop] = fn.bind(target)); + return proxy; + } + const bidRequest = memoize((br) => privateAccessProxy(br, {get}), (arg) => arg.bidId); /** * Return a view on bidd(er) requests where auctionId/transactionId are nulled if the bidder is not allowed `transmitTid`. * @@ -207,7 +216,7 @@ function guardTids(bidderCode) { */ return { bidRequest, - bidderRequest: (br) => new Proxy(br, { + bidderRequest: (br) => privateAccessProxy(br, { get(target, prop, receiver) { if (prop === 'bids') return br.bids.map(bidRequest); return get(target, prop, receiver); @@ -279,14 +288,11 @@ export function newBidder(spec) { onTimelyResponse(spec.code); responses.push(resp) }, - /** Process eventual BidderAuctionResponse.fledgeAuctionConfig field in response. - * @param {Array} fledgeAuctionConfigs - */ onFledgeAuctionConfigs: (fledgeAuctionConfigs) => { fledgeAuctionConfigs.forEach((fledgeAuctionConfig) => { const bidRequest = bidRequestMap[fledgeAuctionConfig.bidId]; if (bidRequest) { - addComponentAuction(bidRequest.adUnitCode, fledgeAuctionConfig.config); + addComponentAuction(bidRequest, fledgeAuctionConfig.config); } else { logWarn('Received fledge auction configuration for an unknown bidId', fledgeAuctionConfig); } @@ -312,7 +318,7 @@ export function newBidder(spec) { bid.originalCpm = bid.cpm; bid.originalCurrency = bid.currency; bid.meta = bid.meta || Object.assign({}, bid[bidRequest.bidder]); - const prebidBid = Object.assign(createBid(CONSTANTS.STATUS.GOOD, bidRequest), bid); + const prebidBid = Object.assign(createBid(CONSTANTS.STATUS.GOOD, bidRequest), bid, pick(bidRequest, TIDS)); addBidWithCode(bidRequest.adUnitCode, prebidBid); } else { logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bid.requestId}. Ignoring.`); @@ -445,6 +451,15 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe onRequest(request); const networkDone = requestMetrics.startTiming('net'); + + function getOptions(defaults) { + const ro = request.options; + return Object.assign(defaults, ro, { + browsingTopics: ro?.hasOwnProperty('browsingTopics') && !ro.browsingTopics + ? false + : isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, activityParams(MODULE_TYPE_BIDDER, spec.code)) + }) + } switch (request.method) { case 'GET': ajax( @@ -454,10 +469,10 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe error: onFailure }, undefined, - Object.assign({ + getOptions({ method: 'GET', withCredentials: true - }, request.options) + }) ); break; case 'POST': @@ -468,11 +483,11 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe error: onFailure }, typeof request.data === 'string' ? request.data : JSON.stringify(request.data), - Object.assign({ + getOptions({ method: 'POST', contentType: 'text/plain', withCredentials: true - }, request.options) + }) ); break; default: @@ -510,7 +525,7 @@ export const registerSyncInner = hook('async', function(spec, responses, gdprCon } }, 'registerSyncs') -export const addComponentAuction = hook('sync', (adUnitCode, fledgeAuctionConfig) => { +export const addComponentAuction = hook('sync', (request, fledgeAuctionConfig) => { }, 'addComponentAuction'); // check that the bid has a width and height set diff --git a/src/adloader.js b/src/adloader.js index 3aae348d106..cd4ad771774 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -24,7 +24,11 @@ const _approvedLoadExternalJSList = [ 'confiant', 'arcspan', 'airgrid', - 'clean.io' + 'clean.io', + 'a1Media', + 'geoedge', + 'mediafilter', + 'qortex' ] /** diff --git a/src/ajax.js b/src/ajax.js index 5e926f3210d..6f2f45e935e 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -1,99 +1,144 @@ -import { config } from './config.js'; -import { logMessage, logError, parseUrl, buildUrl, _each } from './utils.js'; +import {config} from './config.js'; +import {buildUrl, logError, parseUrl} from './utils.js'; -const XHR_DONE = 4; - -/** - * Simple IE9+ and cross-browser ajax request function - * Note: x-domain requests in IE9 do not support the use of cookies - * - * @param url string url - * @param callback {object | function} callback - * @param data mixed data - * @param options object - */ -export const ajax = ajaxBuilder(); - -export function ajaxBuilder(timeout = 3000, {request, done} = {}) { - return function(url, callback, data, options = {}) { - try { - let x; - let method = options.method || (data ? 'POST' : 'GET'); - let parser = document.createElement('a'); - parser.href = url; - - let callbacks = typeof callback === 'object' && callback !== null ? callback : { - success: function() { - logMessage('xhr success'); - }, - error: function(e) { - logError('xhr error', null, e); - } - }; - - if (typeof callback === 'function') { - callbacks.success = callback; - } - - x = new window.XMLHttpRequest(); - - x.onreadystatechange = function () { - if (x.readyState === XHR_DONE) { - if (typeof done === 'function') { - done(parser.origin); - } - let status = x.status; - if ((status >= 200 && status < 300) || status === 304) { - callbacks.success(x.responseText, x); - } else { - callbacks.error(x.statusText, x); - } - } - }; - - // Disabled timeout temporarily to avoid xhr failed requests. https://github.com/prebid/Prebid.js/issues/2648 - if (!config.getConfig('disableAjaxTimeout')) { - x.ontimeout = function () { - logError(' xhr timeout after ', x.timeout, 'ms'); - }; +export const dep = { + fetch: window.fetch.bind(window), + makeRequest: (r, o) => new Request(r, o), + timeout(timeout, resource) { + const ctl = new AbortController(); + let cancelTimer = setTimeout(() => { + ctl.abort(); + logError(`Request timeout after ${timeout}ms`, resource); + cancelTimer = null; + }, timeout); + return { + signal: ctl.signal, + done() { + cancelTimer && clearTimeout(cancelTimer) } + } + } +} - if (method === 'GET' && data) { - let urlInfo = parseUrl(url, options); - Object.assign(urlInfo.search, data); - url = buildUrl(urlInfo); - } +const GET = 'GET'; +const POST = 'POST'; +const CTYPE = 'Content-Type'; - x.open(method, url, true); - // IE needs timeout to be set after open - see #1410 - // Disabled timeout temporarily to avoid xhr failed requests. https://github.com/prebid/Prebid.js/issues/2648 - if (!config.getConfig('disableAjaxTimeout')) { - x.timeout = timeout; - } +/** + * transform legacy `ajax` parameters into a fetch request. + * @returns {Request} + */ +export function toFetchRequest(url, data, options = {}) { + const method = options.method || (data ? POST : GET); + if (method === GET && data) { + const urlInfo = parseUrl(url, options); + Object.assign(urlInfo.search, data); + url = buildUrl(urlInfo); + } + const headers = new Headers(options.customHeaders); + headers.set(CTYPE, options.contentType || 'text/plain'); + const rqOpts = { + method, + headers + } + if (method !== GET && data) { + rqOpts.body = data; + } + if (options.withCredentials) { + rqOpts.credentials = 'include'; + } + if (options.browsingTopics && isSecureContext) { + // the Request constructor will throw an exception if the browser supports topics + // but we're not in a secure context + rqOpts.browsingTopics = true; + } + return dep.makeRequest(url, rqOpts); +} - if (options.withCredentials) { - x.withCredentials = true; - } - _each(options.customHeaders, (value, header) => { - x.setRequestHeader(header, value); - }); - if (options.preflight) { - x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - } - x.setRequestHeader('Content-Type', options.contentType || 'text/plain'); +/** + * Return a version of `fetch` that automatically cancels requests after `timeout` milliseconds. + * + * If provided, `request` and `done` should be functions accepting a single argument. + * `request` is invoked at the beginning of each request, and `done` at the end; both are passed its origin. + * + * @returns {function(*, {}?): Promise} + */ +export function fetcherFactory(timeout = 3000, {request, done} = {}) { + let fetcher = (resource, options) => { + let to; + if (timeout != null && options?.signal == null && !config.getConfig('disableAjaxTimeout')) { + to = dep.timeout(timeout, resource); + options = Object.assign({signal: to.signal}, options); + } + let pm = dep.fetch(resource, options); + if (to?.done != null) pm = pm.finally(to.done); + return pm; + }; - if (typeof request === 'function') { - request(parser.origin); - } + if (request != null || done != null) { + fetcher = ((fetch) => function (resource, options) { + const origin = new URL(resource?.url == null ? resource : resource.url, document.location).origin; + let req = fetch(resource, options); + request && request(origin); + if (done) req = req.finally(() => done(origin)); + return req; + })(fetcher); + } + return fetcher; +} - if (method === 'POST' && data) { - x.send(data); - } else { - x.send(); +function toXHR({status, statusText = '', headers, url}, responseText) { + let xml = 0; + function getXML(onError) { + if (xml === 0) { + try { + xml = new DOMParser().parseFromString(responseText, headers?.get(CTYPE)?.split(';')?.[0]) + } catch (e) { + xml = null; + onError && onError(e) } - } catch (error) { - logError('xhr construction', error); - typeof callback === 'object' && callback !== null && callback.error(error); } + return xml; } + return { + readyState: XMLHttpRequest.DONE, + status, + statusText, + responseText, + response: responseText, + responseType: '', + responseURL: url, + get responseXML() { + return getXML(logError); + }, + getResponseHeader: (header) => headers?.has(header) ? headers.get(header) : null, + toJSON() { + return Object.assign({responseXML: getXML()}, this) + } + } +} + +/** + * attach legacy `ajax` callbacks to a fetch promise. + */ +export function attachCallbacks(fetchPm, callback) { + const {success, error} = typeof callback === 'object' && callback != null ? callback : { + success: typeof callback === 'function' ? callback : () => null, + error: (e, x) => logError('Network error', e, x) + }; + fetchPm.then(response => response.text().then((responseText) => [response, responseText])) + .then(([response, responseText]) => { + const xhr = toXHR(response, responseText); + response.ok || response.status === 304 ? success(responseText, xhr) : error(response.statusText, xhr); + }, () => error('', toXHR({status: 0}, ''))); } + +export function ajaxBuilder(timeout = 3000, {request, done} = {}) { + const fetcher = fetcherFactory(timeout, {request, done}); + return function (url, callback, data, options = {}) { + attachCallbacks(fetcher(toFetchRequest(url, data, options)), callback); + }; +} + +export const ajax = ajaxBuilder(); +export const fetch = fetcherFactory(); diff --git a/src/auction.js b/src/auction.js index 69febccc94e..efce69ea9f8 100644 --- a/src/auction.js +++ b/src/auction.js @@ -4,7 +4,7 @@ * In Prebid 0.x, $$PREBID_GLOBAL$$ had _bidsRequested and _bidsReceived as public properties. * Starting 1.0, Prebid will support concurrent auctions. Each auction instance will store private properties, bidsRequested and bidsReceived. * - * AuctionManager will create instance of auction and will store all the auctions. + * AuctionManager will create an instance of auction and will store all the auctions. * */ @@ -58,11 +58,7 @@ */ import { - _each, - adUnitsFilter, - bind, deepAccess, - flatten, generateUUID, getValue, isEmpty, @@ -90,7 +86,7 @@ import {bidderSettings} from './bidderSettings.js'; import * as events from './events.js'; import adapterManager from './adapterManager.js'; import CONSTANTS from './constants.json'; -import {GreedyPromise} from './utils/promise.js'; +import {defer, GreedyPromise} from './utils/promise.js'; import {useMetrics} from './utils/perfMetrics.js'; import {adjustCpm} from './utils/cpm.js'; import {getGlobal} from './prebidGlobal.js'; @@ -142,7 +138,8 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a const _adUnitCodes = adUnitCodes; const _auctionId = auctionId || generateUUID(); const _timeout = cbTimeout; - const _timelyBidders = new Set(); + const _timelyRequests = new Set(); + const done = defer(); let _bidsRejected = []; let _callback = callback; let _bidderRequests = []; @@ -151,7 +148,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a let _winningBids = []; let _auctionStart; let _auctionEnd; - let _timer; + let _timeoutTimer; let _auctionStatus; let _nonBids = []; @@ -182,25 +179,22 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a } function startAuctionTimer() { - const timedOut = true; - const timeoutCallback = executeCallback.bind(null, timedOut); - let timer = setTimeout(timeoutCallback, _timeout); - _timer = timer; + _timeoutTimer = setTimeout(() => executeCallback(true), _timeout); } - function executeCallback(timedOut, cleartimer) { - // clear timer when done calls executeCallback - if (cleartimer) { - clearTimeout(_timer); + function executeCallback(timedOut) { + if (!timedOut) { + clearTimeout(_timeoutTimer); + } else { + events.emit(CONSTANTS.EVENTS.AUCTION_TIMEOUT, getProperties()); } - if (_auctionEnd === undefined) { - let timedOutBidders = []; + let timedOutRequests = []; if (timedOut) { logMessage(`Auction ${_auctionId} timedOut`); - timedOutBidders = getTimedOutBids(_bidderRequests, _timelyBidders); - if (timedOutBidders.length) { - events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, timedOutBidders); + timedOutRequests = _bidderRequests.filter(rq => !_timelyRequests.has(rq.bidderRequestId)).flatMap(br => br.bids) + if (timedOutRequests.length) { + events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, timedOutRequests); } } @@ -209,14 +203,14 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a metrics.checkpoint('auctionEnd'); metrics.timeBetween('requestBids', 'auctionEnd', 'requestBids.total'); metrics.timeBetween('callBids', 'auctionEnd', 'requestBids.callBids'); + done.resolve(); events.emit(CONSTANTS.EVENTS.AUCTION_END, getProperties()); bidsBackCallback(_adUnits, function () { try { if (_callback != null) { - const adUnitCodes = _adUnitCodes; const bids = _bidsReceived - .filter(bind.call(adUnitsFilter, this, adUnitCodes)) + .filter(bid => _adUnitCodes.includes(bid.adUnitCode)) .reduce(groupByPlacement, {}); _callback.apply(pbjsInstance, [bids, timedOut, _auctionId]); _callback = null; @@ -225,8 +219,8 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a logError('Error executing bidsBackHandler', null, e); } finally { // Calling timed out bidders - if (timedOutBidders.length) { - adapterManager.callTimedOutBidders(adUnits, timedOutBidders, _timeout); + if (timedOutRequests.length) { + adapterManager.callTimedOutBidders(adUnits, timedOutRequests, _timeout); } // Only automatically sync if the publisher has not chosen to "enableOverride" let userSyncConfig = config.getConfig('userSync') || {}; @@ -244,11 +238,11 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a // when all bidders have called done callback atleast once it means auction is complete logInfo(`Bids Received for Auction with id: ${_auctionId}`, _bidsReceived); _auctionStatus = AUCTION_COMPLETED; - executeCallback(false, true); + executeCallback(false); } - function onTimelyResponse(bidderCode) { - _timelyBidders.add(bidderCode); + function onTimelyResponse(bidderRequestId) { + _timelyRequests.add(bidderRequestId); } function callBids() { @@ -386,12 +380,12 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a addBidReceived, addBidRejected, addNoBid, - executeCallback, callBids, addWinningBid, setBidTargeting, getWinningBids: () => _winningBids, getAuctionStart: () => _auctionStart, + getAuctionEnd: () => _auctionEnd, getTimeout: () => _timeout, getAuctionId: () => _auctionId, getAuctionStatus: () => _auctionStatus, @@ -403,6 +397,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a getNonBids: () => _nonBids, getFPD: () => ortb2Fragments, getMetrics: () => metrics, + end: done.promise }; } @@ -417,6 +412,14 @@ export const addBidResponse = hook('sync', function(adUnitCode, bid, reject) { this.dispatch.call(null, adUnitCode, bid); }, 'addBidResponse'); +/** + * Delay hook for adapter responses. + * + * `ready` is a promise; auctions wait for it to resolve before closing. Modules can hook into this + * to delay the end of auctions while they perform initialization that does not need to delay their start. + */ +export const responsesReady = hook('sync', (ready) => ready, 'responsesReady'); + export const addBidderRequests = hook('sync', function(bidderRequests) { this.dispatch.call(this.context, bidderRequests); }, 'addBidderRequests'); @@ -432,32 +435,6 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM let allAdapterCalledDone = false; let bidderRequestsDone = new Set(); let bidResponseMap = {}; - const ready = {}; - - function waitFor(requestId, result) { - if (ready[requestId] == null) { - ready[requestId] = GreedyPromise.resolve(); - } - ready[requestId] = ready[requestId].then(() => GreedyPromise.resolve(result).catch(() => {})) - } - - function guard(bidderRequest, fn) { - let timeout = bidderRequest.timeout; - if (timeout == null || timeout > auctionInstance.getTimeout()) { - timeout = auctionInstance.getTimeout(); - } - const timeRemaining = auctionInstance.getAuctionStart() + timeout - Date.now(); - const wait = ready[bidderRequest.bidderRequestId]; - const orphanWait = ready['']; // also wait for "orphan" responses that are not associated with any request - if ((wait != null || orphanWait != null) && timeRemaining > 0) { - GreedyPromise.race([ - GreedyPromise.timeout(timeRemaining), - GreedyPromise.resolve(orphanWait).then(() => wait) - ]).then(fn); - } else { - fn(); - } - } function afterBidAdded() { outstandingBidsAdded--; @@ -532,8 +509,7 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM return { addBidResponse: (function () { function addBid(adUnitCode, bid) { - const bidderRequest = index.getBidderRequest(bid); - waitFor((bidderRequest && bidderRequest.bidderRequestId) || '', addBidResponse.call({ + addBidResponse.call({ dispatch: acceptBidResponse, }, adUnitCode, bid, (() => { let rejected = false; @@ -543,23 +519,17 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM rejected = true; } } - })())); + })()) } addBid.reject = rejectBidResponse; return addBid; })(), adapterDone: function () { - guard(this, adapterDone.bind(this)) + responsesReady(GreedyPromise.resolve()).finally(() => adapterDone.call(this)); } } } -export function doCallbacksIfTimedout(auctionInstance, bidResponse) { - if (bidResponse.timeToRespond > auctionInstance.getTimeout() + config.getConfig('timeoutBuffer')) { - auctionInstance.executeCallback(true); - } -} - // Add a bid to the auction. export function addBidToAuction(auctionInstance, bidResponse) { setupBidTargeting(bidResponse); @@ -567,8 +537,6 @@ export function addBidToAuction(auctionInstance, bidResponse) { useMetrics(bidResponse.metrics).timeSince('addBidResponse', 'addBidResponse.total'); auctionInstance.addBidReceived(bidResponse); events.emit(CONSTANTS.EVENTS.BID_RESPONSE, bidResponse); - - doCallbacksIfTimedout(auctionInstance, bidResponse); } // Video bids may fail if the cache is down, or there's trouble on the network. @@ -615,16 +583,11 @@ const _storeInCache = (batch) => { const { auctionInstance, bidResponse, afterBidAdded } = batch[i]; if (error) { logWarn(`Failed to save to the video cache: ${error}. Video bid must be discarded.`); - - doCallbacksIfTimedout(auctionInstance, bidResponse); } else { if (cacheId.uuid === '') { logWarn(`Supplied video cache key was already in use by Prebid Cache; caching attempt was rejected. Video bid must be discarded.`); - - doCallbacksIfTimedout(auctionInstance, bidResponse); } else { bidResponse.videoCacheKey = cacheId.uuid; - if (!bidResponse.vastUrl) { bidResponse.vastUrl = getCacheUrl(bidResponse.videoCacheKey); } @@ -820,6 +783,16 @@ export const getPriceByGranularity = (granularity) => { } } +/** + * This function returns a function to get crid from bid response + * @returns {function} + */ +export const getCreativeId = () => { + return (bid) => { + return (bid.creativeId) ? bid.creativeId : ''; + } +} + /** * This function returns a function to get first advertiser domain from bid response meta * @returns {function} @@ -830,6 +803,16 @@ export const getAdvertiserDomain = () => { } } +/** + * This function returns a function to get dsp name or id from bid response meta + * @returns {function} + */ +export const getDSP = () => { + return (bid) => { + return (bid.meta && (bid.meta.networkId || bid.meta.networkName)) ? deepAccess(bid, 'meta.networkName') || deepAccess(bid, 'meta.networkId') : ''; + } +} + /** * This function returns a function to get the primary category id from bid response meta * @returns {function} @@ -866,6 +849,8 @@ function defaultAdserverTargeting() { createKeyVal(TARGETING_KEYS.FORMAT, 'mediaType'), createKeyVal(TARGETING_KEYS.ADOMAIN, getAdvertiserDomain()), createKeyVal(TARGETING_KEYS.ACAT, getPrimaryCatId()), + createKeyVal(TARGETING_KEYS.DSP, getDSP()), + createKeyVal(TARGETING_KEYS.CRID, getCreativeId()), ] } @@ -939,7 +924,7 @@ function setKeys(keyValues, bidderSettings, custBidObj, bidReq) { var targeting = bidderSettings[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING]; custBidObj.size = custBidObj.getSize(); - _each(targeting, function (kvPair) { + (targeting || []).forEach(function (kvPair) { var key = kvPair.key; var value = kvPair.val; @@ -957,7 +942,7 @@ function setKeys(keyValues, bidderSettings, custBidObj, bidReq) { if ( ((typeof bidderSettings.suppressEmptyKeys !== 'undefined' && bidderSettings.suppressEmptyKeys === true) || - key === CONSTANTS.TARGETING_KEYS.DEAL || key === CONSTANTS.TARGETING_KEYS.ACAT) && // hb_deal & hb_acat are suppressed automatically if not set + key === CONSTANTS.TARGETING_KEYS.DEAL || key === CONSTANTS.TARGETING_KEYS.ACAT || key === CONSTANTS.TARGETING_KEYS.DSP || key === CONSTANTS.TARGETING_KEYS.CRID) && // hb_deal & hb_acat are suppressed automatically if not set ( isEmptyStr(value) || value === null || @@ -992,24 +977,3 @@ function groupByPlacement(bidsByPlacement, bid) { bidsByPlacement[bid.adUnitCode].bids.push(bid); return bidsByPlacement; } - -/** - * Returns a list of bids that we haven't received a response yet where the bidder did not call done - * @param {BidRequest[]} bidderRequests List of bids requested for auction instance - * @param {Set} timelyBidders Set of bidders which responded in time - * - * @typedef {Object} TimedOutBid - * @property {string} bidId The id representing the bid - * @property {string} bidder The string name of the bidder - * @property {string} adUnitCode The code used to uniquely identify the ad unit on the publisher's page - * @property {string} auctionId The id representing the auction - * - * @return {Array} List of bids that Prebid hasn't received a response for - */ -function getTimedOutBids(bidderRequests, timelyBidders) { - const timedOutBids = bidderRequests - .map(bid => (bid.bids || []).filter(bid => !timelyBidders.has(bid.bidder))) - .reduce(flatten, []); - - return timedOutBids; -} diff --git a/src/auctionManager.js b/src/auctionManager.js index 90d5fb543e2..498c200ba21 100644 --- a/src/auctionManager.js +++ b/src/auctionManager.js @@ -19,12 +19,16 @@ * @property {function(): void} clearAllAuctions - clear all auctions for testing */ -import { uniques, flatten, logWarn } from './utils.js'; +import { uniques, logWarn } from './utils.js'; import { newAuction, getStandardBidderSettings, AUCTION_COMPLETED } from './auction.js'; -import {find} from './polyfill.js'; import {AuctionIndex} from './auctionIndex.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; +import {ttlCollection} from './utils/ttlCollection.js'; +import {getTTL, onTTLBufferChange} from './bidTTL.js'; +import {config} from './config.js'; + +const CACHE_TTL_SETTING = 'minBidCacheTTL'; /** * Creates new instance of auctionManager. There will only be one instance of auctionManager but @@ -33,15 +37,42 @@ import {useMetrics} from './utils/perfMetrics.js'; * @returns {AuctionManager} auctionManagerInstance */ export function newAuctionManager() { - const _auctions = []; + let minCacheTTL = null; + + const _auctions = ttlCollection({ + startTime: (au) => au.end.then(() => au.getAuctionEnd()), + ttl: (au) => minCacheTTL == null ? null : au.end.then(() => { + return Math.max(minCacheTTL, ...au.getBidsReceived().map(getTTL)) * 1000 + }), + }); + + onTTLBufferChange(() => { + if (minCacheTTL != null) _auctions.refresh(); + }) + + config.getConfig(CACHE_TTL_SETTING, (cfg) => { + const prev = minCacheTTL; + minCacheTTL = cfg?.[CACHE_TTL_SETTING]; + minCacheTTL = typeof minCacheTTL === 'number' ? minCacheTTL : null; + if (prev !== minCacheTTL) { + _auctions.refresh(); + } + }) + const auctionManager = {}; + function getAuction(auctionId) { + for (const auction of _auctions) { + if (auction.getAuctionId() === auctionId) return auction; + } + } + auctionManager.addWinningBid = function(bid) { const metrics = useMetrics(bid.metrics); metrics.checkpoint('bidWon'); metrics.timeBetween('auctionEnd', 'bidWon', 'render.pending'); metrics.timeBetween('requestBids', 'bidWon', 'render.e2e'); - const auction = find(_auctions, auction => auction.getAuctionId() === bid.auctionId); + const auction = getAuction(bid.auctionId); if (auction) { bid.status = CONSTANTS.BID_STATUS.RENDERED; auction.addWinningBid(bid); @@ -50,48 +81,44 @@ export function newAuctionManager() { } }; - auctionManager.getAllWinningBids = function() { - return _auctions.map(auction => auction.getWinningBids()) - .reduce(flatten, []); - }; - - auctionManager.getBidsRequested = function() { - return _auctions.map(auction => auction.getBidRequests()) - .reduce(flatten, []); - }; - - auctionManager.getNoBids = function() { - return _auctions.map(auction => auction.getNoBids()) - .reduce(flatten, []); - }; - - auctionManager.getBidsReceived = function() { - return _auctions.map((auction) => { - if (auction.getAuctionStatus() === AUCTION_COMPLETED) { - return auction.getBidsReceived(); + Object.entries({ + getAllWinningBids: { + name: 'getWinningBids', + }, + getBidsRequested: { + name: 'getBidRequests' + }, + getNoBids: {}, + getAdUnits: {}, + getBidsReceived: { + pre(auction) { + return auction.getAuctionStatus() === AUCTION_COMPLETED; } - }).reduce(flatten, []) - .filter(bid => bid); - }; + }, + getAdUnitCodes: { + post: uniques, + } + }).forEach(([mgrMethod, {name = mgrMethod, pre, post}]) => { + const mapper = pre == null + ? (auction) => auction[name]() + : (auction) => pre(auction) ? auction[name]() : []; + const filter = post == null + ? (items) => items + : (items) => items.filter(post) + auctionManager[mgrMethod] = () => { + return filter(_auctions.toArray().flatMap(mapper)); + } + }) + + function allBidsReceived() { + return _auctions.toArray().flatMap(au => au.getBidsReceived()) + } auctionManager.getAllBidsForAdUnitCode = function(adUnitCode) { - return _auctions.map((auction) => { - return auction.getBidsReceived(); - }).reduce(flatten, []) + return allBidsReceived() .filter(bid => bid && bid.adUnitCode === adUnitCode) }; - auctionManager.getAdUnits = function() { - return _auctions.map(auction => auction.getAdUnits()) - .reduce(flatten, []); - }; - - auctionManager.getAdUnitCodes = function() { - return _auctions.map(auction => auction.getAdUnitCodes()) - .reduce(flatten, []) - .filter(uniques); - }; - auctionManager.createAuction = function(opts) { const auction = newAuction(opts); _addAuction(auction); @@ -99,7 +126,8 @@ export function newAuctionManager() { }; auctionManager.findBidByAdId = function(adId) { - return find(_auctions.map(auction => auction.getBidsReceived()).reduce(flatten, []), bid => bid.adId === adId); + return allBidsReceived() + .find(bid => bid.adId === adId); }; auctionManager.getStandardBidderAdServerTargeting = function() { @@ -111,24 +139,25 @@ export function newAuctionManager() { if (bid) bid.status = status; if (bid && status === CONSTANTS.BID_STATUS.BID_TARGETING_SET) { - const auction = find(_auctions, auction => auction.getAuctionId() === bid.auctionId); + const auction = getAuction(bid.auctionId); if (auction) auction.setBidTargeting(bid); } } auctionManager.getLastAuctionId = function() { - return _auctions.length && _auctions[_auctions.length - 1].getAuctionId() + const auctions = _auctions.toArray(); + return auctions.length && auctions[auctions.length - 1].getAuctionId() }; auctionManager.clearAllAuctions = function() { - _auctions.length = 0; + _auctions.clear(); } function _addAuction(auction) { - _auctions.push(auction); + _auctions.add(auction); } - auctionManager.index = new AuctionIndex(() => _auctions); + auctionManager.index = new AuctionIndex(() => _auctions.toArray()); return auctionManager; } diff --git a/src/bidTTL.js b/src/bidTTL.js new file mode 100644 index 00000000000..55ba0c026b0 --- /dev/null +++ b/src/bidTTL.js @@ -0,0 +1,25 @@ +import {config} from './config.js'; +import {logError} from './utils.js'; +let TTL_BUFFER = 1; + +const listeners = []; + +config.getConfig('ttlBuffer', (cfg) => { + if (typeof cfg.ttlBuffer === 'number') { + const prev = TTL_BUFFER; + TTL_BUFFER = cfg.ttlBuffer; + if (prev !== TTL_BUFFER) { + listeners.forEach(l => l(TTL_BUFFER)) + } + } else { + logError('Invalid value for ttlBuffer', cfg.ttlBuffer); + } +}) + +export function getTTL(bid) { + return bid.ttl - (bid.hasOwnProperty('ttlBuffer') ? bid.ttlBuffer : TTL_BUFFER); +} + +export function onTTLBufferChange(listener) { + listeners.push(listener); +} diff --git a/src/config.js b/src/config.js index 48909d677e9..d4dc07989af 100644 --- a/src/config.js +++ b/src/config.js @@ -12,11 +12,20 @@ * @property {(string|Object)} [video-outstream] */ -import { isValidPriceConfig } from './cpmBucketManager.js'; -import {find, includes, arrayFrom as from} from './polyfill.js'; +import {isValidPriceConfig} from './cpmBucketManager.js'; +import {arrayFrom as from, find, includes} from './polyfill.js'; import { - mergeDeep, deepClone, getParameterByName, isPlainObject, logMessage, logWarn, logError, - isArray, isStr, isBoolean, deepAccess, bind + deepAccess, + deepClone, + getParameterByName, + isArray, + isBoolean, + isPlainObject, + isStr, + logError, + logMessage, + logWarn, + mergeDeep } from './utils.js'; import CONSTANTS from './constants.json'; @@ -505,7 +514,7 @@ export function newConfig() { return function(cb) { return function(...args) { if (typeof cb === 'function') { - return runWithBidder(bidder, bind.call(cb, this, ...args)) + return runWithBidder(bidder, cb.bind(this, ...args)) } else { logWarn('config.callbackWithBidder callback is not a function'); } diff --git a/src/consentHandler.js b/src/consentHandler.js index 4776a8ece02..9e3ee5b4c40 100644 --- a/src/consentHandler.js +++ b/src/consentHandler.js @@ -1,5 +1,6 @@ -import {isStr, timestamp} from './utils.js'; +import {cyrb53Hash, isStr, timestamp} from './utils.js'; import {defer, GreedyPromise} from './utils/promise.js'; +import {config} from './config.js'; /** * Placeholder gvlid for when vendor consent is not required. When this value is used as gvlid, the gdpr @@ -14,7 +15,10 @@ export class ConsentHandler { #data; #defer; #ready; + #dirty = true; + #hash; generatedTime; + hashFields; constructor() { this.reset(); @@ -74,15 +78,24 @@ export class ConsentHandler { setConsentData(data, time = timestamp()) { this.generatedTime = time; + this.#dirty = true; this.#resolve(data); } getConsentData() { return this.#data; } + + get hash() { + if (this.#dirty) { + this.#hash = cyrb53Hash(JSON.stringify(this.#data && this.hashFields ? this.hashFields.map(f => this.#data[f]) : this.#data)) + this.#dirty = false; + } + return this.#hash; + } } -export class UspConsentHandler extends ConsentHandler { +class UspConsentHandler extends ConsentHandler { getConsentMeta() { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { @@ -94,7 +107,8 @@ export class UspConsentHandler extends ConsentHandler { } } -export class GdprConsentHandler extends ConsentHandler { +class GdprConsentHandler extends ConsentHandler { + hashFields = ['gdprApplies', 'consentString'] getConsentMeta() { const consentData = this.getConsentData(); if (consentData && consentData.vendorData && this.generatedTime) { @@ -108,7 +122,8 @@ export class GdprConsentHandler extends ConsentHandler { } } -export class GppConsentHandler extends ConsentHandler { +class GppConsentHandler extends ConsentHandler { + hashFields = ['applicableSections', 'gppString']; getConsentMeta() { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { @@ -159,4 +174,55 @@ export function gvlidRegistry() { } } +export const gdprDataHandler = new GdprConsentHandler(); +export const uspDataHandler = new UspConsentHandler(); +export const gppDataHandler = new GppConsentHandler(); +export const coppaDataHandler = (() => { + function getCoppa() { + return !!(config.getConfig('coppa')) + } + return { + getCoppa, + getConsentData: getCoppa, + getConsentMeta: getCoppa, + reset() {}, + get promise() { + return GreedyPromise.resolve(getCoppa()) + }, + get hash() { + return getCoppa() ? '1' : '0' + } + } +})(); + export const GDPR_GVLIDS = gvlidRegistry(); + +const ALL_HANDLERS = { + gdpr: gdprDataHandler, + usp: uspDataHandler, + gpp: gppDataHandler, + coppa: coppaDataHandler, +} + +export function multiHandler(handlers = ALL_HANDLERS) { + handlers = Object.entries(handlers); + function collector(method) { + return function () { + return Object.fromEntries(handlers.map(([name, handler]) => [name, handler[method]()])) + } + } + return Object.assign( + { + get promise() { + return GreedyPromise.all(handlers.map(([name, handler]) => handler.promise.then(val => [name, val]))) + .then(entries => Object.fromEntries(entries)); + }, + get hash() { + return cyrb53Hash(handlers.map(([_, handler]) => handler.hash).join(':')); + } + }, + Object.fromEntries(['getConsentData', 'getConsentMeta', 'reset'].map(n => [n, collector(n)])), + ) +} + +export const allConsent = multiHandler(); diff --git a/src/constants.json b/src/constants.json index afc4503cbfb..ad0f5b3a71b 100644 --- a/src/constants.json +++ b/src/constants.json @@ -23,6 +23,7 @@ }, "EVENTS": { "AUCTION_INIT": "auctionInit", + "AUCTION_TIMEOUT": "auctionTimeout", "AUCTION_END": "auctionEnd", "BID_ADJUSTMENT": "bidAdjustment", "BID_TIMEOUT": "bidTimeout", @@ -77,7 +78,9 @@ "CACHE_ID": "hb_cache_id", "CACHE_HOST": "hb_cache_host", "ADOMAIN": "hb_adomain", - "ACAT": "hb_acat" + "ACAT": "hb_acat", + "CRID": "hb_crid", + "DSP": "hb_dsp" }, "DEFAULT_TARGETING_KEYS": { "BIDDER": "hb_bidder", @@ -161,11 +164,18 @@ "MAIN": 3 }, "NATIVE_KEYS_THAT_ARE_NOT_ASSETS": [ - "privacyLink", + "privacyIcon", "clickUrl", "sendTargetingKeys", "adTemplate", "rendererUrl", "type" - ] + ], + "FLOOR_VALUES": { + "NO_DATA": "noData", + "AD_UNIT": "adUnit", + "SET_CONFIG": "setConfig", + "FETCH": "fetch", + "SUCCESS": "success" + } } diff --git a/src/events.js b/src/events.js index 62f8c070deb..7a1e25e0e49 100644 --- a/src/events.js +++ b/src/events.js @@ -3,23 +3,38 @@ */ import * as utils from './utils.js' import CONSTANTS from './constants.json'; +import {ttlCollection} from './utils/ttlCollection.js'; +import {config} from './config.js'; +const TTL_CONFIG = 'eventHistoryTTL'; -var slice = Array.prototype.slice; -var push = Array.prototype.push; +let eventTTL = null; -// define entire events -// var allEvents = ['bidRequested','bidResponse','bidWon','bidTimeout']; -var allEvents = utils._map(CONSTANTS.EVENTS, function (v) { - return v; +// keep a record of all events fired +const eventsFired = ttlCollection({ + monotonic: true, + ttl: () => eventTTL, +}) + +config.getConfig(TTL_CONFIG, (val) => { + const previous = eventTTL; + val = val?.[TTL_CONFIG]; + eventTTL = typeof val === 'number' ? val * 1000 : null; + if (previous !== eventTTL) { + eventsFired.refresh(); + } }); -var idPaths = CONSTANTS.EVENT_ID_PATHS; +let slice = Array.prototype.slice; +let push = Array.prototype.push; + +// define entire events +let allEvents = Object.values(CONSTANTS.EVENTS); + +const idPaths = CONSTANTS.EVENT_ID_PATHS; -// keep a record of all events fired -var eventsFired = []; const _public = (function () { - var _handlers = {}; - var _public = {}; + let _handlers = {}; + let _public = {}; /** * @@ -30,18 +45,16 @@ const _public = (function () { function _dispatch(eventString, args) { utils.logMessage('Emitting event for: ' + eventString); - var eventPayload = args[0] || {}; - var idPath = idPaths[eventString]; - var key = eventPayload[idPath]; - var event = _handlers[eventString] || { que: [] }; - var eventKeys = utils._map(event, function (v, k) { - return k; - }); + let eventPayload = args[0] || {}; + let idPath = idPaths[eventString]; + let key = eventPayload[idPath]; + let event = _handlers[eventString] || { que: [] }; + var eventKeys = Object.keys(event); - var callbacks = []; + let callbacks = []; // record the event: - eventsFired.push({ + eventsFired.add({ eventType: eventString, args: eventPayload, id: key, @@ -54,7 +67,7 @@ const _public = (function () { * each function in the `que` array as an argument to push to the * `callbacks` array * */ - if (key && utils.contains(eventKeys, key)) { + if (key && eventKeys.includes(key)) { push.apply(callbacks, event[key].que); } @@ -62,7 +75,7 @@ const _public = (function () { push.apply(callbacks, event.que); /** call each of the callbacks */ - utils._each(callbacks, function (fn) { + (callbacks || []).forEach(function (fn) { if (!fn) return; try { fn.apply(null, args); @@ -73,13 +86,13 @@ const _public = (function () { } function _checkAvailableEvent(event) { - return utils.contains(allEvents, event); + return allEvents.includes(event) } _public.on = function (eventString, handler, id) { // check whether available event or not if (_checkAvailableEvent(eventString)) { - var event = _handlers[eventString] || { que: [] }; + let event = _handlers[eventString] || { que: [] }; if (id) { event[id] = event[id] || { que: [] }; @@ -95,12 +108,12 @@ const _public = (function () { }; _public.emit = function (event) { - var args = slice.call(arguments, 1); + let args = slice.call(arguments, 1); _dispatch(event, args); }; _public.off = function (eventString, handler, id) { - var event = _handlers[eventString]; + let event = _handlers[eventString]; if (utils.isEmpty(event) || (utils.isEmpty(event.que) && utils.isEmpty(event[id]))) { return; @@ -111,15 +124,15 @@ const _public = (function () { } if (id) { - utils._each(event[id].que, function (_handler) { - var que = event[id].que; + (event[id].que || []).forEach(function (_handler) { + let que = event[id].que; if (_handler === handler) { que.splice(que.indexOf(_handler), 1); } }); } else { - utils._each(event.que, function (_handler) { - var que = event.que; + (event.que || []).forEach(function (_handler) { + let que = event.que; if (_handler === handler) { que.splice(que.indexOf(_handler), 1); } @@ -142,13 +155,7 @@ const _public = (function () { * @return {Array} array of events fired */ _public.getEvents = function () { - var arrayCopy = []; - utils._each(eventsFired, function (value) { - var newProp = Object.assign({}, value); - arrayCopy.push(newProp); - }); - - return arrayCopy; + return eventsFired.toArray().map(val => Object.assign({}, val)) }; return _public; @@ -159,5 +166,5 @@ utils._setEventEmitter(_public.emit.bind(_public)); export const {on, off, get, getEvents, emit, addEvents} = _public; export function clearEvents() { - eventsFired.length = 0; + eventsFired.clear(); } diff --git a/src/fpd/sua.js b/src/fpd/sua.js index 30b2be4c13b..565c3e1fd52 100644 --- a/src/fpd/sua.js +++ b/src/fpd/sua.js @@ -15,6 +15,12 @@ export const HIGH_ENTROPY_HINTS = [ 'fullVersionList' ] +export const LOW_ENTROPY_HINTS = [ + 'brands', + 'mobile', + 'platform' +] + /** * Returns low entropy UA client hints encoded as an ortb2.6 device.sua object; or null if no UA client hints are available. */ @@ -32,7 +38,7 @@ export const getLowEntropySUA = lowEntropySUAAccessor(); export const getHighEntropySUA = highEntropySUAAccessor(); export function lowEntropySUAAccessor(uaData = window.navigator?.userAgentData) { - const sua = isEmpty(uaData) ? null : Object.freeze(uaDataToSUA(SUA_SOURCE_LOW_ENTROPY, uaData)); + const sua = (uaData && LOW_ENTROPY_HINTS.some(h => typeof uaData[h] !== 'undefined')) ? Object.freeze(uaDataToSUA(SUA_SOURCE_LOW_ENTROPY, uaData)) : null; return function () { return sua; } @@ -85,7 +91,7 @@ export function uaDataToSUA(source, uaData) { if (uaData.fullVersionList || uaData.brands) { sua.browsers = (uaData.fullVersionList || uaData.brands).map(({brand, version}) => toBrandVersion(brand, version)); } - if (uaData.hasOwnProperty('mobile')) { + if (typeof uaData['mobile'] !== 'undefined') { sua.mobile = uaData.mobile ? 1 : 0; } ['model', 'bitness', 'architecture'].forEach(prop => { diff --git a/src/native.js b/src/native.js index 927423c8d72..c4709dd77e2 100644 --- a/src/native.js +++ b/src/native.js @@ -1,7 +1,6 @@ import { deepAccess, deepClone, - getKeyByValue, insertHtmlIntoIframe, isArray, isBoolean, @@ -422,12 +421,14 @@ function assetsMessage(data, adObject, keys, {index = auctionManager.index} = {} return message; } +const NATIVE_KEYS_INVERTED = Object.fromEntries(Object.entries(CONSTANTS.NATIVE_KEYS).map(([k, v]) => [v, k])); + /** * Constructs a message object containing asset values for each of the * requested data keys. */ export function getAssetMessage(data, adObject) { - const keys = data.assets.map((k) => getKeyByValue(CONSTANTS.NATIVE_KEYS, k)); + const keys = data.assets.map((k) => NATIVE_KEYS_INVERTED[k]); return assetsMessage(data, adObject, keys); } @@ -480,6 +481,11 @@ export function toOrtbNativeRequest(legacyNativeAssets) { continue; } + if (key === 'privacyLink') { + ortb.privacy = 1; + continue; + } + const asset = legacyNativeAssets[key]; let required = 0; if (asset.required && isBoolean(asset.required)) { @@ -623,6 +629,9 @@ export function fromOrtbNativeRequest(openRTBRequest) { oldNativeObject[prebidAssetName].len = asset.data.len; } } + if (openRTBRequest.privacy) { + oldNativeObject.privacyLink = { required: false }; + } // video was not supported by old prebid assets } return oldNativeObject; @@ -696,8 +705,11 @@ export function legacyPropertiesToOrtbNative(legacyNative) { // in general, native trackers seem to be neglected and/or broken response.jstracker = Array.isArray(value) ? value.join('') : value; break; + case 'privacyLink': + response.privacy = value; + break; } - }) + }); return response; } @@ -780,8 +792,8 @@ export function toLegacyResponse(ortbResponse, ortbRequest) { legacyResponse.impressionTrackers = []; let jsTrackers = []; - if (ortbRequest?.imptrackers) { - legacyResponse.impressionTrackers.push(...ortbRequest.imptrackers); + if (ortbResponse.imptrackers) { + legacyResponse.impressionTrackers.push(...ortbResponse.imptrackers); } for (const eventTracker of ortbResponse?.eventtrackers || []) { if (eventTracker.event === TRACKER_EVENTS.impression && eventTracker.method === TRACKER_METHODS.img) { diff --git a/src/prebid.js b/src/prebid.js index b949ece65ea..6ad5120ce82 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -2,17 +2,13 @@ import {getGlobal} from './prebidGlobal.js'; import { - adUnitsFilter, - bind, callBurl, - contains, createInvisibleIframe, deepAccess, deepClone, deepSetValue, flatten, generateUUID, - getHighestCpm, inIframe, insertElement, isArray, @@ -45,12 +41,15 @@ import {executeRenderer, isRendererRequired} from './Renderer.js'; import {createBid} from './bidfactory.js'; import {storageCallbacks} from './storageManager.js'; import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js'; -import {default as adapterManager, gdprDataHandler, getS2SBidderSet, gppDataHandler, uspDataHandler} from './adapterManager.js'; +import {default as adapterManager, getS2SBidderSet} from './adapterManager.js'; import CONSTANTS from './constants.json'; import * as events from './events.js'; import {newMetrics, useMetrics} from './utils/perfMetrics.js'; import {defer, GreedyPromise} from './utils/promise.js'; import {enrichFPD} from './fpd/enrichment.js'; +import {allConsent} from './consentHandler.js'; +import {getHighestCpm} from './utils/reducers.js'; +import {fillVideoDefaults} from './video.js'; const pbjsInstance = getGlobal(); const { triggerUserSyncs } = userSync; @@ -89,7 +88,7 @@ function checkDefinedPlacement(id) { .reduce(flatten) .filter(uniques); - if (!contains(adUnitCodes, id)) { + if (!adUnitCodes.includes(id)) { logError('The "' + id + '" placement is not defined.'); return; } @@ -268,6 +267,12 @@ export const checkAdUnitSetup = hook('sync', function (adUnits) { return validatedAdUnits; }, 'checkAdUnitSetup'); +function fillAdUnitDefaults(adUnits) { + if (FEATURES.VIDEO) { + adUnits.forEach(au => fillVideoDefaults(au)) + } +} + /// /////////////////////////////// // // // Start Public APIs // @@ -330,28 +335,14 @@ pbjsInstance.getAdserverTargeting = function (adUnitCode) { return targeting.getAllTargeting(adUnitCode); }; -/** - * returns all consent data - * @return {Object} Map of consent types and data - * @alias module:pbjs.getConsentData - */ -function getConsentMetadata() { - return { - gdpr: gdprDataHandler.getConsentMeta(), - usp: uspDataHandler.getConsentMeta(), - gpp: gppDataHandler.getConsentMeta(), - coppa: !!(config.getConfig('coppa')) - } -} - pbjsInstance.getConsentMetadata = function () { logInfo('Invoking $$PREBID_GLOBAL$$.getConsentMetadata'); - return getConsentMetadata(); + return allConsent.getConsentMeta() }; function getBids(type) { const responses = auctionManager[type]() - .filter(bind.call(adUnitsFilter, this, auctionManager.getAdUnitCodes())); + .filter(bid => auctionManager.getAdUnitCodes().includes(bid.adUnitCode)) // find the last auction id to get responses for most recent auction only const currentAuctionId = auctionManager.getLastAuctionId(); @@ -671,6 +662,7 @@ pbjsInstance.requestBids = (function() { export const startAuction = hook('async', function ({ bidsBackHandler, timeout: cbTimeout, adUnits, ttlBuffer, adUnitCodes, labels, auctionId, ortb2Fragments, metrics, defer } = {}) { const s2sBidders = getS2SBidderSet(config.getConfig('s2sConfig') || []); + fillAdUnitDefaults(adUnits); adUnits = useMetrics(metrics).measureTime('requestBids.validate', () => checkAdUnitSetup(adUnits)); function auctionDone(bids, timedOut, auctionId) { diff --git a/src/targeting.js b/src/targeting.js index a75c9a2b52f..e46d1ae47a4 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -1,8 +1,6 @@ import { deepAccess, deepClone, - getHighestCpm, - getOldestHighestCpmBid, groupBy, isAdUnitCodeMatchingSlot, isArray, @@ -24,19 +22,12 @@ import {hook} from './hook.js'; import {bidderSettings} from './bidderSettings.js'; import {find, includes} from './polyfill.js'; import CONSTANTS from './constants.json'; +import {getHighestCpm, getOldestHighestCpmBid} from './utils/reducers.js'; +import {getTTL} from './bidTTL.js'; var pbTargetingKeys = []; const MAX_DFP_KEYLENGTH = 20; -let DEFAULT_TTL_BUFFER = 1; - -config.getConfig('ttlBuffer', (cfg) => { - if (typeof cfg.ttlBuffer === 'number') { - DEFAULT_TTL_BUFFER = cfg.ttlBuffer; - } else { - logError('Invalid value for ttlBuffer', cfg.ttlBuffer); - } -}) const CFG_ALLOW_TARGETING_KEYS = `targetingControls.allowTargetingKeys`; const CFG_ADD_TARGETING_KEYS = `targetingControls.addTargetingKeys`; @@ -47,7 +38,7 @@ export const TARGETING_KEYS = Object.keys(CONSTANTS.TARGETING_KEYS).map( ); // return unexpired bids -const isBidNotExpired = (bid) => (bid.responseTimestamp + (bid.ttl - (bid.hasOwnProperty('ttlBuffer') ? bid.ttlBuffer : DEFAULT_TTL_BUFFER)) * 1000) > timestamp(); +const isBidNotExpired = (bid) => (bid.responseTimestamp + getTTL(bid) * 1000) > timestamp(); // return bids whose status is not set. Winning bids can only have a status of `rendered`. const isUnusedBid = (bid) => bid && ((bid.status && !includes([CONSTANTS.BID_STATUS.RENDERED], bid.status)) || !bid.status); @@ -479,6 +470,12 @@ export function newTargeting(auctionManager) { .filter(bid => deepAccess(bid, 'video.context') !== ADPOD) .filter(isBidUsable); + bidsReceived + .forEach(bid => { + bid.latestTargetedAuctionId = latestAuctionForAdUnit[bid.adUnitCode]; + return bid; + }); + return getHighestCpmBidsFromBidPool(bidsReceived, getOldestHighestCpmBid); } diff --git a/src/utils.js b/src/utils.js index ece29732723..256dfb15174 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,6 @@ import {config} from './config.js'; import clone from 'just-clone'; -import {find, includes} from './polyfill.js'; +import {includes} from './polyfill.js'; import CONSTANTS from './constants.json'; import {GreedyPromise} from './utils/promise.js'; import {getGlobal} from './prebidGlobal.js'; @@ -8,7 +8,6 @@ import {getGlobal} from './prebidGlobal.js'; export { default as deepAccess } from 'dlv/index.js'; export { dset as deepSetValue } from 'dset'; -var tArr = 'Array'; var tStr = 'String'; var tFn = 'Function'; var tNumb = 'Number'; @@ -64,17 +63,6 @@ export function getPrebidInternal() { return prebidInternal; } -var uniqueRef = {}; -export let bind = function(a, b) { return b; }.bind(null, 1, uniqueRef)() === uniqueRef - ? Function.prototype.bind - : function(bind) { - var self = this; - var args = Array.prototype.slice.call(arguments, 1); - return function() { - return self.apply(bind, args.concat(Array.prototype.slice.call(arguments))); - }; - }; - /* utility method to get incremental integer starting from 1 */ var getIncrementalInteger = (function () { var count = 0; @@ -114,19 +102,7 @@ function _getRandomData() { } export function getBidIdParameter(key, paramsObj) { - if (paramsObj && paramsObj[key]) { - return paramsObj[key]; - } - - return ''; -} - -export function tryAppendQueryString(existingUrl, key, value) { - if (value) { - return existingUrl + key + '=' + encodeURIComponent(value) + '&'; - } - - return existingUrl; + return paramsObj?.[key] || ''; } // parse a query string object passed in bid params @@ -145,84 +121,30 @@ export function parseQueryStringParameters(queryObj) { export function transformAdServerTargetingObj(targeting) { // we expect to receive targeting for a single slot at a time if (targeting && Object.getOwnPropertyNames(targeting).length > 0) { - return getKeys(targeting) - .map(key => `${key}=${encodeURIComponent(getValue(targeting, key))}`).join('&'); + return Object.keys(targeting) + .map(key => `${key}=${encodeURIComponent(targeting[key])}`).join('&'); } else { return ''; } } -/** - * Read an adUnit object and return the sizes used in an [[728, 90]] format (even if they had [728, 90] defined) - * Preference is given to the `adUnit.mediaTypes.banner.sizes` object over the `adUnit.sizes` - * @param {object} adUnit one adUnit object from the normal list of adUnits - * @returns {Array.} array of arrays containing numeric sizes - */ -export function getAdUnitSizes(adUnit) { - if (!adUnit) { - return; - } - - let sizes = []; - if (adUnit.mediaTypes && adUnit.mediaTypes.banner && Array.isArray(adUnit.mediaTypes.banner.sizes)) { - let bannerSizes = adUnit.mediaTypes.banner.sizes; - if (Array.isArray(bannerSizes[0])) { - sizes = bannerSizes; - } else { - sizes.push(bannerSizes); - } - // TODO - remove this else block when we're ready to deprecate adUnit.sizes for bidders - } else if (Array.isArray(adUnit.sizes)) { - if (Array.isArray(adUnit.sizes[0])) { - sizes = adUnit.sizes; - } else { - sizes.push(adUnit.sizes); - } - } - return sizes; -} - /** * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']' * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]` */ export function parseSizesInput(sizeObj) { - var parsedSizes = []; - - // if a string for now we can assume it is a single size, like "300x250" if (typeof sizeObj === 'string') { // multiple sizes will be comma-separated - var sizes = sizeObj.split(','); - - // regular expression to match strigns like 300x250 - // start of line, at least 1 number, an "x" , then at least 1 number, and the then end of the line - var sizeRegex = /^(\d)+x(\d)+$/i; - if (sizes) { - for (var curSizePos in sizes) { - if (hasOwn(sizes, curSizePos) && sizes[curSizePos].match(sizeRegex)) { - parsedSizes.push(sizes[curSizePos]); - } - } - } + return sizeObj.split(',').filter(sz => sz.match(/^(\d)+x(\d)+$/i)) } else if (typeof sizeObj === 'object') { - var sizeArrayLength = sizeObj.length; - - // don't process empty array - if (sizeArrayLength > 0) { - // if we are a 2 item array of 2 numbers, we must be a SingleSize array - if (sizeArrayLength === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') { - parsedSizes.push(parseGPTSingleSizeArray(sizeObj)); - } else { - // otherwise, we must be a MultiSize array - for (var i = 0; i < sizeArrayLength; i++) { - parsedSizes.push(parseGPTSingleSizeArray(sizeObj[i])); - } - } + if (sizeObj.length === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') { + return [parseGPTSingleSizeArray(sizeObj)]; + } else { + return sizeObj.map(parseGPTSingleSizeArray) } } - - return parsedSizes; + return []; } // Parse a GPT style single size array, (i.e [300, 250]) @@ -345,6 +267,9 @@ export function createInvisibleIframe() { f.frameBorder = '0'; f.src = 'about:blank'; f.style.display = 'none'; + f.style.height = '0px'; + f.style.width = '0px'; + f.allowtransparency = 'true'; return f; } @@ -375,9 +300,7 @@ export function isStr(object) { return isA(object, tStr); } -export function isArray(object) { - return isA(object, tArr); -} +export const isArray = Array.isArray.bind(Array); export function isNumber(object) { return isA(object, tNumb); @@ -402,12 +325,7 @@ export function isEmpty(object) { if (isArray(object) || isStr(object)) { return !(object.length > 0); } - - for (var k in object) { - if (hasOwnProperty.call(object, k)) return false; - } - - return true; + return Object.keys(object).length <= 0; } /** @@ -426,38 +344,12 @@ export function isEmptyStr(str) { * @param {Function(value, key, object)} fn */ export function _each(object, fn) { - if (isEmpty(object)) return; - if (isFn(object.forEach)) return object.forEach(fn, this); - - var k = 0; - var l = object.length; - - if (l > 0) { - for (; k < l; k++) fn(object[k], k, object); - } else { - for (k in object) { - if (hasOwnProperty.call(object, k)) fn.call(this, object[k], k); - } - } + if (isFn(object?.forEach)) return object.forEach(fn, this); + Object.entries(object || {}).forEach(([k, v]) => fn.call(this, v, k)); } export function contains(a, obj) { - if (isEmpty(a)) { - return false; - } - - if (isFn(a.indexOf)) { - return a.indexOf(obj) !== -1; - } - - var i = a.length; - while (i--) { - if (a[i] === obj) { - return true; - } - } - - return false; + return isFn(a?.includes) && a.includes(obj); } /** @@ -468,24 +360,10 @@ export function contains(a, obj) { * @return {Array} */ export function _map(object, callback) { - if (isEmpty(object)) return []; - if (isFn(object.map)) return object.map(callback); - var output = []; - _each(object, function (value, key) { - output.push(callback(value, key, object)); - }); - - return output; + if (isFn(object?.map)) return object.map(callback); + return Object.entries(object || {}).map(([k, v]) => callback(v, k, object)) } -export function hasOwn(objectToCheck, propertyToCheckFor) { - if (objectToCheck.hasOwnProperty) { - return objectToCheck.hasOwnProperty(propertyToCheckFor); - } else { - return (typeof objectToCheck[propertyToCheckFor] !== 'undefined') && (objectToCheck.constructor.prototype[propertyToCheckFor] !== objectToCheck[propertyToCheckFor]); - } -}; - /* * Inserts an element(elm) as targets child, by default as first child * @param {HTMLElement} elm @@ -568,27 +446,14 @@ export function insertHtmlIntoIframe(htmlCode) { if (!htmlCode) { return; } - - let iframe = document.createElement('iframe'); - iframe.id = getUniqueIdentifierStr(); - iframe.width = 0; - iframe.height = 0; - iframe.hspace = '0'; - iframe.vspace = '0'; - iframe.marginWidth = '0'; - iframe.marginHeight = '0'; - iframe.style.display = 'none'; - iframe.style.height = '0px'; - iframe.style.width = '0px'; - iframe.scrolling = 'no'; - iframe.frameBorder = '0'; - iframe.allowtransparency = 'true'; - + const iframe = createInvisibleIframe(); internal.insertElement(iframe, document, 'body'); - iframe.contentWindow.document.open(); - iframe.contentWindow.document.write(htmlCode); - iframe.contentWindow.document.close(); + ((doc) => { + doc.open(); + doc.write(htmlCode); + doc.close(); + })(iframe.contentWindow.document); } /** @@ -654,19 +519,6 @@ export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') `; } -export function getValueString(param, val, defaultValue) { - if (val === undefined || val === null) { - return defaultValue; - } - if (isStr(val)) { - return val; - } - if (isNumber(val)) { - return val.toString(); - } - internal.logWarn('Unsuported type for param: ' + param + ' required type: String'); -} - export function uniques(value, index, arry) { return arry.indexOf(value) === index; } @@ -679,38 +531,14 @@ export function getBidRequest(id, bidderRequests) { if (!id) { return; } - let bidRequest; - bidderRequests.some(bidderRequest => { - let result = find(bidderRequest.bids, bid => ['bidId', 'adId', 'bid_id'].some(type => bid[type] === id)); - if (result) { - bidRequest = result; - } - return result; - }); - return bidRequest; -} - -export function getKeys(obj) { - return Object.keys(obj); + return bidderRequests.flatMap(br => br.bids) + .find(bid => ['bidId', 'adId', 'bid_id'].some(prop => bid[prop] === id)) } export function getValue(obj, key) { return obj[key]; } -/** - * Get the key of an object for a given value - */ -export function getKeyByValue(obj, value) { - for (let prop in obj) { - if (obj.hasOwnProperty(prop)) { - if (obj[prop] === value) { - return prop; - } - } - } -} - export function getBidderCodes(adUnits = pbjsInstance.adUnits) { // this could memoize adUnits return adUnits.map(unit => unit.bids.map(bid => bid.bidder) @@ -729,26 +557,6 @@ export function isApnGetTagDefined() { } } -// This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond -export const getHighestCpm = getHighestCpmCallback('timeToRespond', (previous, current) => previous > current); - -// This function will get the oldest hightest cpm value bid, in case of tie it will return the bid which came in first -// Use case for tie: https://github.com/prebid/Prebid.js/issues/2448 -export const getOldestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous > current); - -// This function will get the latest hightest cpm value bid, in case of tie it will return the bid which came in last -// Use case for tie: https://github.com/prebid/Prebid.js/issues/2539 -export const getLatestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous < current); - -function getHighestCpmCallback(useTieBreakerProperty, tieBreakerCallback) { - return (previous, current) => { - if (previous.cpm === current.cpm) { - return tieBreakerCallback(previous[useTieBreakerProperty], current[useTieBreakerProperty]) ? current : previous; - } - return previous.cpm < current.cpm ? current : previous; - } -} - /** * Fisher–Yates shuffle * http://stackoverflow.com/a/6274398 @@ -775,10 +583,6 @@ export function shuffle(array) { return array; } -export function adUnitsFilter(filter, bid) { - return includes(filter, bid && bid.adUnitCode); -} - export function deepClone(obj) { return clone(obj); } @@ -925,8 +729,7 @@ export function isValidMediaTypes(mediaTypes) { export function getUserConfiguredParams(adUnits, adUnitCode, bidder) { return adUnits .filter(adUnit => adUnit.code === adUnitCode) - .map((adUnit) => adUnit.bids) - .reduce(flatten, []) + .flatMap((adUnit) => adUnit.bids) .filter((bidderData) => bidderData.bidder === bidder) .map((bidderData) => bidderData.params || {}); } @@ -938,7 +741,7 @@ export function getDNT() { return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNotTrack === '1' || navigator.doNotTrack === 'yes'; } -const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; +export const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; /** * Returns filter function to match adUnitCode in slot @@ -949,41 +752,6 @@ export function isAdUnitCodeMatchingSlot(slot) { return (adUnitCode) => compareCodeAndSlot(slot, adUnitCode); } -/** - * Returns filter function to match adUnitCode in slot - * @param {string} adUnitCode AdUnit code - * @return {function} filter function - */ -export function isSlotMatchingAdUnitCode(adUnitCode) { - return (slot) => compareCodeAndSlot(slot, adUnitCode); -} - -/** - * @summary Uses the adUnit's code in order to find a matching gpt slot object on the page - */ -export function getGptSlotForAdUnitCode(adUnitCode) { - let matchingSlot; - if (isGptPubadsDefined()) { - // find the first matching gpt slot on the page - matchingSlot = find(window.googletag.pubads().getSlots(), isSlotMatchingAdUnitCode(adUnitCode)); - } - return matchingSlot; -}; - -/** - * @summary Uses the adUnit's code in order to find a matching gptSlot on the page - */ -export function getGptSlotInfoForAdUnitCode(adUnitCode) { - const matchingSlot = getGptSlotForAdUnitCode(adUnitCode); - if (matchingSlot) { - return { - gptSlot: matchingSlot.getAdUnitPath(), - divId: matchingSlot.getSlotElementId() - } - } - return {}; -}; - /** * Constructs warning message for when unsupported bidders are dropped from an adunit * @param {Object} adUnit ad unit from which the bidder is being dropped @@ -1005,33 +773,14 @@ export function unsupportedBidderMessage(adUnit, bidder) { * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger * @param {*} value */ -export function isInteger(value) { - if (Number.isInteger) { - return Number.isInteger(value); - } else { - return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; - } -} - -/** - * Converts a string value in camel-case to underscore eg 'placementId' becomes 'placement_id' - * @param {string} value string value to convert - */ -export function convertCamelToUnderscore(value) { - return value.replace(/(?:^|\.?)([A-Z])/g, function (x, y) { return '_' + y.toLowerCase() }).replace(/^_/, ''); -} +export const isInteger = Number.isInteger.bind(Number); /** * Returns a new object with undefined properties removed from given object * @param obj the object to clean */ export function cleanObj(obj) { - return Object.keys(obj).reduce((newObj, key) => { - if (typeof obj[key] !== 'undefined') { - newObj[key] = obj[key]; - } - return newObj; - }, {}) + return Object.fromEntries(Object.entries(obj).filter(([_, v]) => typeof v !== 'undefined')) } /** @@ -1068,104 +817,10 @@ export function pick(obj, properties) { }, {}); } -/** - * Try to convert a value to a type. - * If it can't be done, the value will be returned. - * - * @param {string} typeToConvert The target type. e.g. "string", "number", etc. - * @param {*} value The value to be converted into typeToConvert. - */ -function tryConvertType(typeToConvert, value) { - if (typeToConvert === 'string') { - return value && value.toString(); - } else if (typeToConvert === 'number') { - return Number(value); - } else { - return value; - } -} - -export function convertTypes(types, params) { - Object.keys(types).forEach(key => { - if (params[key]) { - if (isFn(types[key])) { - params[key] = types[key](params[key]); - } else { - params[key] = tryConvertType(types[key], params[key]); - } - - // don't send invalid values - if (isNaN(params[key])) { - delete params.key; - } - } - }); - return params; -} - export function isArrayOfNums(val, size) { return (isArray(val)) && ((size) ? val.length === size : true) && (val.every(v => isInteger(v))); } -/** - * Creates an array of n length and fills each item with the given value - */ -export function fill(value, length) { - let newArray = []; - - for (let i = 0; i < length; i++) { - let valueToPush = isPlainObject(value) ? deepClone(value) : value; - newArray.push(valueToPush); - } - - return newArray; -} - -/** - * http://npm.im/chunk - * Returns an array with *size* chunks from given array - * - * Example: - * ['a', 'b', 'c', 'd', 'e'] chunked by 2 => - * [['a', 'b'], ['c', 'd'], ['e']] - */ -export function chunk(array, size) { - let newArray = []; - - for (let i = 0; i < Math.ceil(array.length / size); i++) { - let start = i * size; - let end = start + size; - newArray.push(array.slice(start, end)); - } - - return newArray; -} - -export function getMinValueFromArray(array) { - return Math.min(...array); -} - -export function getMaxValueFromArray(array) { - return Math.max(...array); -} - -/** - * This function will create compare function to sort on object property - * @param {string} property - * @returns {function} compare function to be used in sorting - */ -export function compareOn(property) { - return function compare(a, b) { - if (a[property] < b[property]) { - return 1; - } - if (a[property] > b[property]) { - return -1; - } - return 0; - } -} - export function parseQS(query) { return !query ? {} : query .replace(/^\?/, '') @@ -1328,15 +983,6 @@ export function cyrb53Hash(str, seed = 0) { return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(); } -/** - * returns a window object, which holds the provided document or null - * @param {Document} doc - * @returns {Window} - */ -export function getWindowFromDocument(doc) { - return (doc) ? doc.defaultView : null; -} - /** * returns the result of `JSON.parse(data)`, or undefined if that throws an error. * @param data @@ -1375,36 +1021,33 @@ export function memoize(fn, key = function (arg) { return arg; }) { * @param {object} attributes */ export function setScriptAttributes(script, attributes) { - for (let key in attributes) { - if (attributes.hasOwnProperty(key)) { - script.setAttribute(key, attributes[key]); - } - } + Object.entries(attributes).forEach(([k, v]) => script.setAttribute(k, v)) } /** - * Encode a string for inclusion in HTML. - * See https://pragmaticwebsecurity.com/articles/spasecurity/json-stringify-xss.html and - * https://codeql.github.com/codeql-query-help/javascript/js-bad-code-sanitization/ - * @return {string} + * Perform a binary search for `el` on an ordered array `arr`. + * + * @returns the lowest nonnegative integer I that satisfies: + * key(arr[i]) >= key(el) for each i between I and arr.length + * + * (if one or more matches are found for `el`, returns the index of the first; + * if the element is not found, return the index of the first element that's greater; + * if no greater element exists, return `arr.length`) */ -export const escapeUnsafeChars = (() => { - const escapes = { - '<': '\\u003C', - '>': '\\u003E', - '/': '\\u002F', - '\\': '\\\\', - '\b': '\\b', - '\f': '\\f', - '\n': '\\n', - '\r': '\\r', - '\t': '\\t', - '\0': '\\0', - '\u2028': '\\u2028', - '\u2029': '\\u2029' - }; - - return function(str) { - return str.replace(/[<>\b\f\n\r\t\0\u2028\u2029\\]/g, x => escapes[x]) +export function binarySearch(arr, el, key = (el) => el) { + let left = 0; + let right = arr.length && arr.length - 1; + const target = key(el); + while (right - left > 1) { + const middle = left + Math.round((right - left) / 2); + if (target > key(arr[middle])) { + left = middle; + } else { + right = middle; + } } -})(); + while (arr.length > left && target > key(arr[left])) { + left++; + } + return left; +} diff --git a/src/utils/currency.js b/src/utils/currency.js deleted file mode 100644 index ab7e5faa1ea..00000000000 --- a/src/utils/currency.js +++ /dev/null @@ -1,16 +0,0 @@ -import {getGlobal} from '../prebidGlobal.js'; - -/** - * "best effort" wrapper around currency conversion; always returns an amount that may or may not be correct. - */ -export function beConvertCurrency(amount, from, to) { - if (from === to) return amount; - let result = amount; - if (typeof getGlobal().convertCurrency === 'function') { - try { - result = getGlobal().convertCurrency(amount, from, to); - } catch (e) { - } - } - return result; -} diff --git a/src/utils/reducers.js b/src/utils/reducers.js new file mode 100644 index 00000000000..28851be8aaa --- /dev/null +++ b/src/utils/reducers.js @@ -0,0 +1,44 @@ +export function simpleCompare(a, b) { + if (a === b) return 0; + return a < b ? -1 : 1; +} + +export function keyCompare(key = (item) => item) { + return (a, b) => simpleCompare(key(a), key(b)) +} + +export function reverseCompare(compare = simpleCompare) { + return (a, b) => -compare(a, b) || 0; +} + +export function tiebreakCompare(...compares) { + return function (a, b) { + for (const cmp of compares) { + const val = cmp(a, b); + if (val !== 0) return val; + } + return 0; + } +} + +export function minimum(compare = simpleCompare) { + return (min, item) => compare(item, min) < 0 ? item : min; +} + +export function maximum(compare = simpleCompare) { + return minimum(reverseCompare(compare)); +} + +const cpmCompare = keyCompare((bid) => bid.cpm); +const timestampCompare = keyCompare((bid) => bid.responseTimestamp); + +// This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond +export const getHighestCpm = maximum(tiebreakCompare(cpmCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond)))) + +// This function will get the oldest hightest cpm value bid, in case of tie it will return the bid which came in first +// Use case for tie: https://github.com/prebid/Prebid.js/issues/2448 +export const getOldestHighestCpmBid = maximum(tiebreakCompare(cpmCompare, reverseCompare(timestampCompare))) + +// This function will get the latest hightest cpm value bid, in case of tie it will return the bid which came in last +// Use case for tie: https://github.com/prebid/Prebid.js/issues/2539 +export const getLatestHighestCpmBid = maximum(tiebreakCompare(cpmCompare, timestampCompare)) diff --git a/src/utils/ttlCollection.js b/src/utils/ttlCollection.js new file mode 100644 index 00000000000..392ed1c9ad7 --- /dev/null +++ b/src/utils/ttlCollection.js @@ -0,0 +1,139 @@ +import {GreedyPromise} from './promise.js'; +import {binarySearch, timestamp} from '../utils.js'; + +/** + * Create a set-like collection that automatically forgets items after a certain time. + * + * @param {({}) => Number|Promise} startTime? a function taking an item added to this collection, + * and returning (a promise to) a timestamp to be used as the starting time for the item + * (the item will be dropped after `ttl(item)` milliseconds have elapsed since this timestamp). + * Defaults to the time the item was added to the collection. + * @param {({}) => Number|void|Promise} ttl a function taking an item added to this collection, + * and returning (a promise to) the duration (in milliseconds) the item should be kept in it. + * May return null to indicate that the item should be persisted indefinitely. + * @param {boolean} monotonic? set to true for better performance, but only if, given any two items A and B in this collection: + * if A was added before B, then: + * - startTime(A) + ttl(A) <= startTime(B) + ttl(B) + * - Promise.all([startTime(A), ttl(A)]) never resolves later than Promise.all([startTime(B), ttl(B)]) + * @param {number} slack? maximum duration (in milliseconds) that an item is allowed to persist + * once past its TTL. This is also roughly the interval between "garbage collection" sweeps. + */ +export function ttlCollection( + { + startTime = timestamp, + ttl = () => null, + monotonic = false, + slack = 5000 + } = {} +) { + const items = new Map(); + const pendingPurge = []; + const markForPurge = monotonic + ? (entry) => pendingPurge.push(entry) + : (entry) => pendingPurge.splice(binarySearch(pendingPurge, entry, (el) => el.expiry), 0, entry) + let nextPurge, task; + + function reschedulePurge() { + task && clearTimeout(task); + if (pendingPurge.length > 0) { + const now = timestamp(); + nextPurge = Math.max(now, pendingPurge[0].expiry + slack); + task = setTimeout(() => { + const now = timestamp(); + let cnt = 0; + for (const entry of pendingPurge) { + if (entry.expiry > now) break; + items.delete(entry.item) + cnt++; + } + pendingPurge.splice(0, cnt); + task = null; + reschedulePurge(); + }, nextPurge - now); + } else { + task = null; + } + } + + function mkEntry(item) { + const values = {}; + const thisCohort = currentCohort; + let expiry; + + function update() { + if (thisCohort === currentCohort && values.start != null && values.delta != null) { + expiry = values.start + values.delta; + markForPurge(entry); + if (task == null || nextPurge > expiry + slack) { + reschedulePurge(); + } + } + } + + const [init, refresh] = Object.entries({ + start: startTime, + delta: ttl + }).map(([field, getter]) => { + let currentCall; + return function() { + const thisCall = currentCall = {}; + GreedyPromise.resolve(getter(item)).then((val) => { + if (thisCall === currentCall) { + values[field] = val; + update(); + } + }); + } + }) + + const entry = { + item, + refresh, + get expiry() { + return expiry; + }, + }; + + init(); + refresh(); + return entry; + } + + let currentCohort = {}; + + return { + [Symbol.iterator]: () => items.keys(), + /** + * Add an item to this collection. + * @param item + */ + add(item) { + !items.has(item) && items.set(item, mkEntry(item)); + }, + /** + * Clear this collection. + */ + clear() { + pendingPurge.length = 0; + reschedulePurge(); + items.clear(); + currentCohort = {}; + }, + /** + * @returns {[]} all the items in this collection, in insertion order. + */ + toArray() { + return Array.from(items.keys()); + }, + /** + * Refresh the TTL for each item in this collection. + */ + refresh() { + pendingPurge.length = 0; + reschedulePurge(); + for (const entry of items.values()) { + entry.refresh(); + } + }, + }; +} diff --git a/src/video.js b/src/video.js index 7930e318874..ff137892a2b 100644 --- a/src/video.js +++ b/src/video.js @@ -1,25 +1,21 @@ -import adapterManager from './adapterManager.js'; -import { deepAccess, logError } from './utils.js'; -import { config } from '../src/config.js'; -import {includes} from './polyfill.js'; -import { hook } from './hook.js'; +import {deepAccess, logError} from './utils.js'; +import {config} from '../src/config.js'; +import {hook} from './hook.js'; import {auctionManager} from './auctionManager.js'; -const VIDEO_MEDIA_TYPE = 'video'; export const OUTSTREAM = 'outstream'; export const INSTREAM = 'instream'; -/** - * Helper functions for working with video-enabled adUnits - */ -export const videoAdUnit = adUnit => { - const mediaType = adUnit.mediaType === VIDEO_MEDIA_TYPE; - const mediaTypes = deepAccess(adUnit, 'mediaTypes.video'); - return mediaType || mediaTypes; -}; -export const videoBidder = bid => includes(adapterManager.videoAdapters, bid.bidder); -export const hasNonVideoBidder = adUnit => - adUnit.bids.filter(bid => !videoBidder(bid)).length; +export function fillVideoDefaults(adUnit) { + const video = adUnit?.mediaTypes?.video; + if (video != null && video.plcmt == null) { + if (video.context === OUTSTREAM || [2, 3, 4].includes(video.placement)) { + video.plcmt = 4; + } else if (video.context !== OUTSTREAM && [2, 6].includes(video.playbackmethod)) { + video.plcmt = 2; + } + } +} /** * @typedef {object} VideoBid diff --git a/test/mocks/xhr.js b/test/mocks/xhr.js index 424100f870c..e7b1d96f0a4 100644 --- a/test/mocks/xhr.js +++ b/test/mocks/xhr.js @@ -1,12 +1,236 @@ import {getUniqueIdentifierStr} from '../../src/utils.js'; +import {GreedyPromise} from '../../src/utils/promise.js'; +import {fakeXhr} from 'nise'; +import {dep} from 'src/ajax.js'; -export let server = sinon.createFakeServer(); -export let xhr = global.XMLHttpRequest; +export const xhr = sinon.useFakeXMLHttpRequest(); +export const server = mockFetchServer(); -beforeEach(function() { - server.restore(); - server = sinon.createFakeServer(); - xhr = global.XMLHttpRequest; +/** + * An (incomplete) replica of nise's fakeServer, but backing fetch used in ajax.js (rather than XHR). + */ +function mockFetchServer() { + const sandbox = sinon.createSandbox(); + const bodies = new WeakMap(); + const requests = []; + const {DONE, UNSENT} = XMLHttpRequest; + + function makeRequest(resource, options) { + const requestBody = options?.body || bodies.get(resource); + const request = new Request(resource, options); + bodies.set(request, requestBody); + return request; + } + + function mockXHR(resource, options) { + let resolve, reject; + const promise = new GreedyPromise((res, rej) => { + resolve = res; + reject = rej; + }); + + function error(reason = new TypeError('Failed to fetch')) { + mockReq.status = 0; + reject(reason); + } + + const request = makeRequest(resource, options); + request.signal.onabort = () => error(new DOMException('The user aborted a request')); + let responseHeaders; + + const mockReq = { + fetch: { + request, + requestBody: bodies.get(request), + promise, + }, + readyState: UNSENT, + url: request.url, + method: request.method, + requestBody: bodies.get(request), + status: 0, + statusText: '', + requestHeaders: new Proxy(request.headers, { + get(target, prop) { + return typeof prop === 'string' && target.has(prop) ? target.get(prop) : {}[prop]; + }, + has(target, prop) { + return typeof prop === 'string' && target.has(prop); + }, + ownKeys(target) { + return Array.from(target.keys()); + }, + getOwnPropertyDescriptor(target, prop) { + if (typeof prop === 'string' && target.has(prop)) { + return { + enumerable: true, + configurable: true, + writable: false, + value: target.get(prop) + } + } + } + }), + withCredentials: request.credentials === 'include', + setStatus(status) { + // nise replaces invalid status with 200 + status = typeof status === 'number' ? status : 200; + mockReq.status = status; + mockReq.statusText = fakeXhr.FakeXMLHttpRequest.statusCodes[status] || ''; + }, + setResponseHeaders(headers) { + responseHeaders = headers; + }, + setResponseBody(body) { + if (mockReq.status === 0) { + error(); + return; + } + const resp = Object.defineProperties(new Response(body, { + status: mockReq.status, + statusText: mockReq.statusText, + headers: responseHeaders || {}, + }), { + url: { + get: () => mockReq.fetch.request.url, + } + }); + mockReq.readyState = DONE; + // tests expect respond() to run everything immediately, + // so make body available syncronously + resp.text = () => GreedyPromise.resolve(body || ''); + Object.assign(mockReq.fetch, { + response: resp, + responseBody: body || '' + }) + resolve(resp); + }, + respond(status = 200, headers, body) { + mockReq.setStatus(status); + mockReq.setResponseHeaders(headers); + mockReq.setResponseBody(body); + }, + error + }; + return mockReq; + } + + let enabled = false; + let timeoutsEnabled = false; + + function enable() { + if (!enabled) { + sandbox.stub(dep, 'fetch').callsFake((resource, options) => { + const req = mockXHR(resource, options); + requests.push(req); + return req.fetch.promise; + }); + sandbox.stub(dep, 'makeRequest').callsFake(makeRequest); + const timeout = dep.timeout; + sandbox.stub(dep, 'timeout').callsFake(function () { + if (timeoutsEnabled) { + return timeout.apply(null, arguments); + } else { + return {}; + } + }); + enabled = true; + } + } + + enable(); + + const responders = []; + + function respondWith() { + let response, urlMatcher, methodMatcher; + urlMatcher = methodMatcher = () => true; + switch (arguments.length) { + case 1: + ([response] = arguments); + break; + case 2: + ([urlMatcher, response] = arguments); + break; + case 3: + ([methodMatcher, urlMatcher, response] = arguments); + methodMatcher = ((toMatch) => (method) => method === toMatch)(methodMatcher); + break; + default: + throw new Error('Invalid respondWith invocation'); + } + if (typeof urlMatcher.exec === 'function') { + urlMatcher = ((rx) => (url) => rx.exec(url)?.slice(1))(urlMatcher); + } else if (typeof urlMatcher === 'string') { + urlMatcher = ((toMatch) => (url) => url === toMatch)(urlMatcher); + } + responders.push((req) => { + if (req.readyState !== DONE && methodMatcher(req.method)) { + const arg = urlMatcher(req.url); + if (arg) { + if (typeof response === 'function') { + response(req, ...(Array.isArray(arg) ? arg : [])); + } else if (typeof response === 'string') { + req.respond(200, null, response); + } else { + req.respond.apply(req, response); + } + } + } + }); + } + + function resetState() { + requests.length = 0; + responders.length = 0; + timeoutsEnabled = false; + } + + return { + requests, + enable, + restore() { + resetState(); + sandbox.restore(); + enabled = false; + }, + reset() { + sandbox.resetHistory(); + resetState(); + }, + respondWith, + respond() { + if (arguments.length > 0) { + respondWith.apply(null, arguments); + } + requests.forEach(req => { + for (let i = responders.length - 1; i >= 0; i--) { + responders[i](req); + if (req.readyState === DONE) break; + } + if (req.readyState !== DONE) { + req.respond(404, {}, ''); + } + }); + }, + /** + * the timeout mechanism is quite different between XHR and fetch + * by default, mocked fetch does not time out - to reflect fakeServer XHRs + * note that many tests will fire requests without caring or waiting for their response - + * if they are timed out later, during unrelated tests, the log messages might interfere with their + * assertions + */ + get autoTimeout() { + return timeoutsEnabled; + }, + set autoTimeout(val) { + timeoutsEnabled = !!val; + } + }; +} + +beforeEach(function () { + server.reset(); }); const bid = getUniqueIdentifierStr().substring(4); @@ -20,12 +244,35 @@ afterEach(function () { return (s) => s.split('\n').map(s => `${preamble} ${s}`).join('\n'); })(); + function format(obj, body = null) { + if (obj == null) return obj; + const fmt = {}; + let node = obj; + while (node != null) { + Object.keys(node).forEach((k) => { + const val = obj[k]; + if (typeof val !== 'function' && !fmt.hasOwnProperty(k)) { + fmt[k] = val; + } + }); + node = Object.getPrototypeOf(node); + } + if (obj.headers != null) { + fmt.headers = Object.fromEntries(obj.headers.entries()) + } + fmt.body = body; + return fmt; + } + - console.log(prepend(`XHR mock state after failure (for test '${this.currentTest.fullTitle()}'): ${server.requests.length} requests`)) + console.log(prepend(`XHR mock state after failure (for test '${this.currentTest.fullTitle()}'): ${server.requests.length} requests`)); server.requests.forEach((req, i) => { console.log(prepend(`Request #${i}:`)); - console.log(prepend(JSON.stringify(req, null, 2))); - }) + console.log(prepend(JSON.stringify({ + request: format(req.fetch.request, req.fetch.requestBody), + response: format(req.fetch.response, req.fetch.responseBody) + }, null, 2))); + }); } }); /* eslint-enable */ diff --git a/test/spec/appnexusKeywords_spec.js b/test/spec/appnexusKeywords_spec.js index 9bf567a27c5..68faeff0b82 100644 --- a/test/spec/appnexusKeywords_spec.js +++ b/test/spec/appnexusKeywords_spec.js @@ -1,4 +1,4 @@ -import {transformBidderParamKeywords} from '../../libraries/appnexusKeywords/anKeywords.js'; +import {transformBidderParamKeywords} from '../../libraries/appnexusUtils/anKeywords.js'; import {expect} from 'chai/index.js'; import * as utils from '../../src/utils.js'; diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index dd59b16d492..137d009bd18 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -5,7 +5,7 @@ import { adjustBids, getMediaTypeGranularity, getPriceByGranularity, - addBidResponse + addBidResponse, resetAuctionState, responsesReady } from 'src/auction.js'; import CONSTANTS from 'src/constants.json'; import * as auctionModule from 'src/auction.js'; @@ -23,6 +23,7 @@ import {AuctionIndex} from '../../src/auctionIndex.js'; import {expect} from 'chai'; import {deepClone} from '../../src/utils.js'; import { IMAGE as ortbNativeRequest } from 'src/native.js'; +import {PrebidServer} from '../../modules/prebidServerBidAdapter/index.js'; var assert = require('assert'); @@ -48,6 +49,7 @@ function mockBid(opts) { let bidderCode = opts && opts.bidderCode; return { + adUnitCode: opts?.adUnitCode || ADUNIT_CODE, 'ad': 'creative', 'cpm': '1.99', 'width': 300, @@ -68,6 +70,11 @@ function mockBid(opts) { transactionId: this.transactionId, auctionId: this.auctionId } + }, + _ctx: { + adUnits: opts?.adUnits, + src: opts?.src, + uniquePbsTid: opts?.uniquePbsTid, } }; } @@ -96,6 +103,9 @@ function mockBidRequest(bid, opts) { 'bidderCode': bidderCode || bid.bidderCode, 'auctionId': opts && opts.auctionId, 'bidderRequestId': requestId, + src: bid?._ctx?.src, + adUnitsS2SCopy: bid?._ctx?.src === CONSTANTS.S2S.SRC ? bid?._ctx?.adUnits : undefined, + uniquePbsTid: bid?._ctx?.src === CONSTANTS.S2S.SRC ? bid?._ctx?.uniquePbsTid : undefined, 'bids': [ { 'bidder': bidderCode || bid.bidderCode, @@ -108,7 +118,8 @@ function mockBidRequest(bid, opts) { 'bidId': bid.requestId, 'bidderRequestId': requestId, 'auctionId': opts && opts.auctionId, - 'mediaTypes': mediaType + 'mediaTypes': mediaType, + src: bid?._ctx?.src } ], 'auctionStart': 1505250713622, @@ -160,6 +171,7 @@ describe('auctionmanager.js', function () { indexAuctions = []; indexStub = sinon.stub(auctionManager, 'index'); indexStub.get(() => new AuctionIndex(() => indexAuctions)); + resetAuctionState(); }); afterEach(() => { @@ -185,9 +197,11 @@ describe('auctionmanager.js', function () { adId: '1adId', source: 'client', mediaType: 'banner', + creativeId: 'monkeys', meta: { advertiserDomains: ['adomain'], - primaryCatId: 'IAB-test' + primaryCatId: 'IAB-test', + networkId: '123987' } }; @@ -202,6 +216,8 @@ describe('auctionmanager.js', function () { expected[ CONSTANTS.TARGETING_KEYS.FORMAT ] = bid.mediaType; expected[ CONSTANTS.TARGETING_KEYS.ADOMAIN ] = bid.meta.advertiserDomains[0]; expected[ CONSTANTS.TARGETING_KEYS.ACAT ] = bid.meta.primaryCatId; + expected[ CONSTANTS.TARGETING_KEYS.DSP ] = bid.meta.networkId; + expected[ CONSTANTS.TARGETING_KEYS.CRID ] = bid.creativeId; if (bid.mediaType === 'video') { expected[ CONSTANTS.TARGETING_KEYS.UUID ] = bid.videoCacheKey; expected[ CONSTANTS.TARGETING_KEYS.CACHE_ID ] = bid.videoCacheKey; @@ -304,6 +320,18 @@ describe('auctionmanager.js', function () { return bidResponse.meta.advertiserDomains[0]; } }, + { + key: CONSTANTS.TARGETING_KEYS.CRID, + val: function (bidResponse) { + return bidResponse.creativeId; + } + }, + { + key: CONSTANTS.TARGETING_KEYS.DSP, + val: function (bidResponse) { + return bidResponse.meta.networkId; + } + }, { key: CONSTANTS.TARGETING_KEYS.ACAT, val: function (bidResponse) { @@ -388,6 +416,18 @@ describe('auctionmanager.js', function () { return bidResponse.meta.advertiserDomains[0]; } }, + { + key: CONSTANTS.TARGETING_KEYS.CRID, + val: function (bidResponse) { + return bidResponse.creativeId; + } + }, + { + key: CONSTANTS.TARGETING_KEYS.DSP, + val: function (bidResponse) { + return bidResponse.meta.networkId; + } + }, { key: CONSTANTS.TARGETING_KEYS.ACAT, val: function (bidResponse) { @@ -487,7 +527,7 @@ describe('auctionmanager.js', function () { s2sConfig: { accountId: '1', enabled: true, - defaultVendor: 'appnexus', + defaultVendor: 'appnexuspsp', bidders: ['appnexus'], timeout: 1000, adapter: 'prebidServer' @@ -735,9 +775,10 @@ describe('auctionmanager.js', function () { }); describe('createAuction', () => { - let adUnits, stubMakeBidRequests, stubCallAdapters + let adUnits, stubMakeBidRequests, stubCallAdapters, bids; beforeEach(() => { + bids = []; stubMakeBidRequests = sinon.stub(adapterManager, 'makeBidRequests').returns([{ bidderCode: BIDDER_CODE, bids: [{ @@ -745,6 +786,7 @@ describe('auctionmanager.js', function () { }] }]); stubCallAdapters = sinon.stub(adapterManager, 'callBids').callsFake((au, reqs, addBid, done) => { + bids.forEach(bid => addBid(bid.adUnitCode, bid)); reqs.forEach(r => done.apply(r)); }); adUnits = [{ @@ -759,6 +801,7 @@ describe('auctionmanager.js', function () { afterEach(() => { stubMakeBidRequests.restore(); stubCallAdapters.restore(); + auctionManager.clearAllAuctions(); }); it('passes global and bidder ortb2 to the auction', () => { @@ -786,6 +829,79 @@ describe('auctionmanager.js', function () { }); expect(auction.getNonBids()[0]).to.equal('test'); }); + + describe('stale auctions', () => { + let clock, auction; + beforeEach(() => { + clock = sinon.useFakeTimers(); + auction = auctionManager.createAuction({adUnits}); + indexAuctions.push(auction); + }); + afterEach(() => { + clock.restore(); + config.resetConfig(); + }); + + it('are dropped after their last bid becomes stale (if minBidCacheTTL is set)', () => { + config.setConfig({ + minBidCacheTTL: 0 + }); + bids = [ + { + adUnitCode: ADUNIT_CODE, + transactionId: ADUNIT_CODE, + ttl: 10 + }, { + adUnitCode: ADUNIT_CODE, + transactionId: ADUNIT_CODE, + ttl: 100 + } + ]; + auction.callBids(); + return auction.end.then(() => { + clock.tick(50 * 1000); + expect(auctionManager.getBidsReceived().length).to.equal(2); + clock.tick(56 * 1000); + expect(auctionManager.getBidsReceived()).to.eql([]); + }); + }); + + it('are dropped after `minBidCacheTTL` seconds if they had no bid', () => { + auction.callBids(); + config.setConfig({ + minBidCacheTTL: 2 + }); + return auction.end.then(() => { + expect(auctionManager.getNoBids().length).to.eql(1); + clock.tick(10 * 10000); + expect(auctionManager.getNoBids().length).to.eql(0); + }) + }); + + Object.entries({ + 'bids': { + bd: [{ + adUnitCode: ADUNIT_CODE, + transactionId: ADUNIT_CODE, + ttl: 10 + }], + entries: () => auctionManager.getBidsReceived() + }, + 'no bids': { + bd: [], + entries: () => auctionManager.getNoBids() + } + }).forEach(([t, {bd, entries}]) => { + it(`with ${t} are never dropped if minBidCacheTTL is not set`, () => { + bids = bd; + auction.callBids(); + return auction.end.then(() => { + clock.tick(100 * 1000); + expect(entries().length > 0).to.be.true; + }) + }) + }); + }) }); describe('addBidResponse #1', function () { @@ -811,6 +927,11 @@ describe('auctionmanager.js', function () { beforeEach(function () { ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(mockAjaxBuilder); adUnits = [{ + mediaTypes: { + banner: { + sizes: [] + } + }, code: ADUNIT_CODE, transactionId: ADUNIT_CODE, bids: [ @@ -996,36 +1117,47 @@ describe('auctionmanager.js', function () { assert.strictEqual(addedBid.renderer.url, myBid.renderer.url); }); - it('bid for a regular unit and a video unit', function() { - let renderer = { - url: 'renderer.js', - render: (bid) => bid - }; - Object.assign(adUnits[0], {renderer}); - // make sure that if the renderer is only on the second ad unit, prebid - // still correctly uses it - let bid = mockBid(); - let bidRequests = [mockBidRequest(bid, {auctionId: auction.getAuctionId()})]; - - bidRequests[0].bids[1] = Object.assign({ - bidId: utils.getUniqueIdentifierStr() - }, bidRequests[0].bids[0]); - Object.assign(bidRequests[0].bids[0], { - adUnitCode: ADUNIT_CODE1, - transactionId: ADUNIT_CODE1, - }); + describe('bid for a regular unit and a video unit', () => { + beforeEach(() => { + const renderer = { + url: 'renderer.js', + render: (bid) => bid + }; + Object.assign(adUnits[0], {renderer}); + // make sure that if the renderer is only on the second ad unit, prebid + // still correctly uses it + let bid = mockBid(); + let bidRequests = [mockBidRequest(bid, {auctionId: auction.getAuctionId()})]; + + bidRequests[0].bids[1] = Object.assign({ + bidId: utils.getUniqueIdentifierStr() + }, bidRequests[0].bids[0]); + Object.assign(bidRequests[0].bids[0], { + adUnitCode: ADUNIT_CODE1, + transactionId: ADUNIT_CODE1, + }); - makeRequestsStub.returns(bidRequests); + makeRequestsStub.returns(bidRequests); - // this should correspond with the second bid in the bidReq because of the ad unit code - bid.mediaType = 'video-outstream'; - spec.interpretResponse.returns(bid); + // this should correspond with the second bid in the bidReq because of the ad unit code + bid.mediaType = 'video-outstream'; + spec.interpretResponse.returns(bid); + }); - auction.callBids(); + it('should use renderers on bid response', () => { + auction.callBids(); - const addedBid = find(auction.getBidsReceived(), bid => bid.adUnitCode == ADUNIT_CODE); - assert.equal(addedBid.renderer.url, 'renderer.js'); - }); + const addedBid = find(auction.getBidsReceived(), bid => bid.adUnitCode === ADUNIT_CODE); + assert.equal(addedBid.renderer.url, 'renderer.js'); + }); + + it('should resolve .end', () => { + auction.callBids(); + return auction.end.then(() => { + expect(auction.getBidsReceived().length).to.eql(1); + }) + }); + }) it('sets bidResponse.ttlBuffer from adUnit.ttlBuffer', () => { adUnits[0].ttlBuffer = 0; @@ -1035,12 +1167,19 @@ describe('auctionmanager.js', function () { }); describe('when auction timeout is 20', function () { - let eventsEmitSpy; + let eventsEmitSpy, auctionDone; - function setupBids(auctionId) { - bids = [mockBid(), mockBid({ bidderCode: BIDDER_CODE1 })]; - let bidRequests = bids.map(bid => mockBidRequest(bid, {auctionId})); + function respondToRequest(requestIndex) { + server.requests[requestIndex].respond(200, {}, 'response body'); + } + + function runAuction() { + let bidRequests = bids.map(bid => mockBidRequest(bid, {auctionId: auction.getAuctionId()})); makeRequestsStub.returns(bidRequests); + return new Promise((resolve) => { + auctionDone = resolve; + auction.callBids(); + }) } beforeEach(function () { @@ -1049,86 +1188,156 @@ describe('auctionmanager.js', function () { transactionId: ADUNIT_CODE, bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, + {bidder: BIDDER_CODE1, params: {placementId: 'id'}}, ] }]; adUnitCodes = [ADUNIT_CODE]; eventsEmitSpy = sinon.spy(events, 'emit'); + bids = [mockBid(), mockBid({ bidderCode: BIDDER_CODE1 })]; + const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); + registerBidder(spec1); + const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); + registerBidder(spec2); + auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: () => auctionDone(), cbTimeout: 20}); + indexAuctions = [auction]; }); + afterEach(function () { events.emit.restore(); }); - it('should emit BID_TIMEOUT and AUCTION_END for timed out bids', function (done) { - const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); - registerBidder(spec2); + it('resolves .end on timeout', () => { + let endResolved = false; + auction.end.then(() => { + endResolved = true; + }) + const pm = runAuction().then(() => { + expect(endResolved).to.be.true; + }); + respondToRequest(0); + return pm; + }); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + describe('AUCTION_TIMEOUT event', () => { + let handler; + beforeEach(() => { + handler = sinon.spy(); + events.on(CONSTANTS.EVENTS.AUCTION_TIMEOUT, handler); + }) + afterEach(() => { + events.off(CONSTANTS.EVENTS.AUCTION_TIMEOUT, handler); + }); + + Object.entries({ + 'is fired on timeout': [true, [0]], + 'is NOT fired otherwise': [false, [0, 1]], + }).forEach(([t, [shouldFire, respond]]) => { + it(t, () => { + const pm = runAuction().then(() => { + if (shouldFire) { + sinon.assert.calledWith(handler, sinon.match({auctionId: auction.getAuctionId()})) + } else { + sinon.assert.notCalled(handler); + } + }); + respond.forEach(respondToRequest); + return pm; + }) + }); + }); + + it('should emit BID_TIMEOUT and AUCTION_END for timed out bids', function () { + const pm = runAuction().then(() => { const bidTimeoutCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0]; const timedOutBids = bidTimeoutCall.args[1]; assert.equal(timedOutBids.length, 1); assert.equal(timedOutBids[0].bidder, BIDDER_CODE1); // Check that additional properties are available - assert.equal(timedOutBids[0].params.placementId, 'id'); + assert.equal(timedOutBids[0].params[0].placementId, 'id'); const auctionEndCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.AUCTION_END).getCalls()[0]; const auctionProps = auctionEndCall.args[1]; assert.equal(auctionProps.adUnits, adUnits); assert.equal(auctionProps.timeout, 20); assert.equal(auctionProps.auctionStatus, AUCTION_COMPLETED) - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - - auction.callBids(); + }); respondToRequest(0); + return pm; }); - it('should NOT emit BID_TIMEOUT when all bidders responded in time', function (done) { - const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); - registerBidder(spec2); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + it('should NOT emit BID_TIMEOUT when all bidders responded in time', function () { + const pm = runAuction().then(() => { assert.ok(eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).notCalled, 'did not emit event BID_TIMEOUT'); - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - auction.callBids(); + }); respondToRequest(0); respondToRequest(1); + return pm; }); - it('should NOT emit BID_TIMEOUT for bidders which responded in time but with an empty bid', function (done) { - const spec1 = mockBidder(BIDDER_CODE, []); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, []); - registerBidder(spec2); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + it('should NOT emit BID_TIMEOUT for bidders which responded in time but with an empty bid', function () { + const pm = runAuction().then(() => { const bidTimeoutCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0]; const timedOutBids = bidTimeoutCall.args[1]; assert.equal(timedOutBids.length, 1); assert.equal(timedOutBids[0].bidder, BIDDER_CODE1); - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - auction.callBids(); + }); respondToRequest(0); + return pm; }); + + it('should NOT emit BID_TIMEOUT for bidders that replied through S2S', () => { + adapterManager.registerBidAdapter(new PrebidServer(), 'pbs'); + config.setConfig({ + s2sConfig: [{ + accountId: '1', + enabled: true, + defaultVendor: 'appnexuspsp', + bidders: ['mock-s2s-1'], + adapter: 'pbs' + }, { + accountId: '1', + enabled: true, + defaultVendor: 'rubicon', + bidders: ['mock-s2s-2'], + adapter: 'pbs' + }] + }) + adUnits[0].bids.push({bidder: 'mock-s2s-1'}, {bidder: 'mock-s2s-2'}) + const s2sAdUnits = deepClone(adUnits); + bids.unshift( + mockBid({bidderCode: 'mock-s2s-1', src: CONSTANTS.S2S.SRC, adUnits: s2sAdUnits, uniquePbsTid: '1'}), + mockBid({bidderCode: 'mock-s2s-2', src: CONSTANTS.S2S.SRC, adUnits: s2sAdUnits, uniquePbsTid: '2'}) + ); + Object.assign(s2sAdUnits[0], { + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [ + { + bidder: 'mock-s2s-1', + bid_id: bids[0].requestId + }, + { + bidder: 'mock-s2s-2', + bid_id: bids[1].requestId + } + ] + }) + + const pm = runAuction().then(() => { + const toBids = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0].args[1] + expect(toBids.map(bid => bid.bidder)).to.eql([ + 'mock-s2s-2', + BIDDER_CODE, + BIDDER_CODE1, + ]) + }); + respondToRequest(1); + return pm; + }) }); }); @@ -1623,116 +1832,54 @@ describe('auctionmanager.js', function () { sinon.assert.calledWith(auction.addBidReceived, sinon.match({cpm: 1.23})); }) - describe('when addBidResponse hook returns promises', () => { - let resolvers, callbacks, bids; + describe('when responsesReady defers', () => { + let resolve, reject, promise, callbacks, bids; - function hook(next, ...args) { - next.bail(new Promise((resolve, reject) => { - resolvers.resolve.push(resolve); - resolvers.reject.push(reject); - }).finally(() => next(...args))); + function hook(next, ready) { + next(ready.then(() => promise)); } - function invokeCallbacks() { - bids.forEach((bid) => callbacks.addBidResponse(ADUNIT_CODE, bid)); - bidRequests.forEach(bidRequest => callbacks.adapterDone.call(bidRequest)); - } + before(() => { + responsesReady.before(hook); + }); - function delay(ms = 0) { - return new Promise((resolve) => { - setTimeout(resolve, ms) - }); - } + after(() => { + responsesReady.getHooks({hook}).remove(); + }); beforeEach(() => { + // eslint-disable-next-line promise/param-names + promise = new Promise((rs, rj) => { + resolve = rs; + reject = rj; + }); bids = [ mockBid({bidderCode: BIDDER_CODE1}), mockBid({bidderCode: BIDDER_CODE}) ] bidRequests = bids.map((b) => mockBidRequest(b)); - resolvers = {resolve: [], reject: []}; - addBidResponse.before(hook); callbacks = auctionCallbacks(doneSpy, auction); Object.assign(auction, { addNoBid: sinon.spy() }); }); - afterEach(() => { - addBidResponse.getHooks({hook: hook}).remove(); - }); - - it('should wait for bids without a request bids before calling auctionDone', () => { - callbacks.addBidResponse(ADUNIT_CODE, Object.assign(mockBid(), {requestId: null})); - invokeCallbacks(); - resolvers.resolve.slice(1, 3).forEach((fn) => fn()); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - resolvers.resolve[0](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - Object.entries({ - 'all succeed': ['resolve', 'resolve'], - 'some fail': ['resolve', 'reject'], - 'all fail': ['reject', 'reject'] - }).forEach(([test, results]) => { - describe(`(and ${test})`, () => { - it('should wait for them to complete before calling auctionDone', () => { - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - expect(auction.addNoBid.called).to.be.false; - resolvers[results[0]][0](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.false; - expect(auction.addNoBid.called).to.be.false; - resolvers[results[1]][1](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); + 'resolve': () => resolve(), + 'reject': () => reject(), + }).forEach(([t, resolver]) => { + it(`should wait for responsesReady to ${t} before calling auctionDone`, (done) => { + bidRequests.forEach(bidRequest => callbacks.adapterDone.call(bidRequest)); + setTimeout(() => { + sinon.assert.notCalled(doneSpy); + resolver(); + setTimeout(() => { + sinon.assert.called(doneSpy); + done(); + }) + }) }); }); - - Object.entries({ - bidder: (timeout) => { - bidRequests.forEach((r) => r.timeout = timeout); - auction.getTimeout = () => timeout + 10000 - }, - auction: (timeout) => { - auction.getTimeout = () => timeout; - bidRequests.forEach((r) => r.timeout = timeout + 10000) - } - }).forEach(([test, setTimeout]) => { - it(`should respect ${test} timeout if they never complete`, () => { - const start = Date.now() - 2900; - auction.getAuctionStart = () => start; - setTimeout(3000); - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - return delay(100); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - - it(`should not wait if ${test} has already timed out`, () => { - const start = Date.now() - 2000; - auction.getAuctionStart = () => start; - setTimeout(1000); - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - }) }); describe('when bids are rejected', () => { diff --git a/test/spec/fpd/enrichment_spec.js b/test/spec/fpd/enrichment_spec.js index 328846ca081..3b3afb15f8c 100644 --- a/test/spec/fpd/enrichment_spec.js +++ b/test/spec/fpd/enrichment_spec.js @@ -275,7 +275,13 @@ describe('FPD enrichment', () => { describe('sua', () => { it('does not set device.sua if resolved sua is null', () => { - sandbox.stub(dep, 'getHighEntropySUA').returns(Promise.resolve()) + sandbox.stub(dep, 'getHighEntropySUA').returns(Promise.resolve()); + // Add hints so it will attempt to retrieve high entropy values + config.setConfig({ + firstPartyData: { + uaHints: ['bitness'], + } + }); return fpd().then(ortb2 => { expect(ortb2.device.sua).to.not.exist; }) diff --git a/test/spec/fpd/sua_spec.js b/test/spec/fpd/sua_spec.js index 431f47268d3..63e0068d0ef 100644 --- a/test/spec/fpd/sua_spec.js +++ b/test/spec/fpd/sua_spec.js @@ -165,6 +165,14 @@ describe('uaDataToSUA', () => { }); describe('lowEntropySUAAccessor', () => { + // Set up a mock data with readonly property + class MockUserAgentData {} + Object.defineProperty(MockUserAgentData.prototype, 'mobile', { + value: false, + writable: false, + enumerable: true + }); + function getSUA(uaData) { return lowEntropySUAAccessor(uaData)(); } @@ -181,6 +189,10 @@ describe('lowEntropySUAAccessor', () => { it('should return null if uaData is empty', () => { expect(getSUA({})).to.eql(null); }) + + it('should return mobile and source', () => { + expect(getSUA(new MockUserAgentData())).to.eql({mobile: 0, source: 1}) + }) }); describe('highEntropySUAAccessor', () => { diff --git a/test/spec/libraries/cmp/cmpClient_spec.js b/test/spec/libraries/cmp/cmpClient_spec.js new file mode 100644 index 00000000000..adbbbf5cb1d --- /dev/null +++ b/test/spec/libraries/cmp/cmpClient_spec.js @@ -0,0 +1,296 @@ +import {cmpClient, MODE_CALLBACK, MODE_RETURN} from '../../../../libraries/cmp/cmpClient.js'; + +describe('cmpClient', () => { + function mockWindow(props = {}) { + let listeners = []; + const win = { + addEventListener: sinon.stub().callsFake((evt, listener) => { + evt === 'message' && listeners.push(listener) + }), + removeEventListener: sinon.stub().callsFake((evt, listener) => { + evt === 'message' && (listeners = listeners.filter((l) => l !== listener)); + }), + postMessage: sinon.stub().callsFake((msg) => { + listeners.forEach(ln => ln({data: msg})) + }), + ...props, + }; + win.top = win.parent?.top || win; + return win; + } + + it('should return undefined when there is no CMP', () => { + expect(cmpClient({apiName: 'missing'}, mockWindow())).to.not.exist; + }); + + it('should return undefined when parent is inaccessible', () => { + const win = mockWindow(); + win.top = mockWindow(); + expect(cmpClient({apiName: 'missing'}, win)).to.not.exist; + }) + + describe('direct access', () => { + let mockApiFn; + beforeEach(() => { + mockApiFn = sinon.stub(); + }) + Object.entries({ + 'on same frame': () => mockWindow({mockApiFn}), + 'on parent frame': () => mockWindow({parent: mockWindow({parent: mockWindow({parent: mockWindow(), mockApiFn})})}), + }).forEach(([t, mkWindow]) => { + describe(t, () => { + let win, mkClient; + beforeEach(() => { + win = mkWindow(); + mkClient = (opts) => cmpClient(Object.assign({apiName: 'mockApiFn'}, opts), win) + }); + + it('should mark client function as direct', () => { + expect(mkClient().isDirect).to.equal(true); + }); + + it('should find and call the CMP api function', () => { + mkClient()({command: 'mockCmd'}); + sinon.assert.calledWith(mockApiFn, 'mockCmd'); + }); + + describe('should return a promise that', () => { + let cbResult; + beforeEach(() => { + cbResult = []; + mockApiFn.callsFake((cmd, callback) => { + if (typeof callback === 'function') { + callback.apply(this, cbResult); + } + return 'val' + }) + }) + + Object.entries({ + callback: [sinon.stub(), 'undefined', undefined], + 'callback, mode = MODE_CALLBACK': [sinon.stub(), 'undefined', undefined, MODE_CALLBACK], + 'callback, mode = MODE_RETURN': [sinon.stub(), 'api return value', 'val', MODE_RETURN], + 'no callback': [undefined, 'api return value', 'val'], + 'no callback, mode = MODE_CALLBACK': [undefined, 'callback arg', 'cbVal', MODE_CALLBACK], + 'no callback, mode = MODE_RETURN': [undefined, 'api return value', 'val', MODE_RETURN], + }).forEach(([t, [callback, tResult, expectedResult, mode]]) => { + describe(`when ${t} is provided`, () => { + Object.entries({ + 'no success flag': undefined, + 'success is set': true + }).forEach(([t, success]) => { + it(`resolves to ${tResult} (${t})`, (done) => { + cbResult = ['cbVal', success]; + mkClient({mode})({callback}).then((val) => { + expect(val).to.equal(expectedResult); + done(); + }) + }); + + it('should pass either a function or undefined as callback', () => { + mkClient({mode})({callback}); + sinon.assert.calledWith(mockApiFn, sinon.match.any, sinon.match(arg => typeof arg === 'undefined' || typeof arg === 'function')) + }) + }); + }) + }); + + it('rejects to undefined when callback is provided and success = false', (done) => { + cbResult = ['cbVal', false]; + mkClient()({callback: sinon.stub()}).catch(val => { + expect(val).to.not.exist; + done(); + }) + }); + + it('rejects to callback arg when callback is NOT provided, success = false, mode = MODE_CALLBACK', (done) => { + cbResult = ['cbVal', false]; + mkClient({mode: MODE_CALLBACK})().catch(val => { + expect(val).to.eql('cbVal'); + done(); + }) + }) + + it('rejects when CMP api throws', (done) => { + mockApiFn.reset(); + const e = new Error(); + mockApiFn.throws(e); + mkClient()({}).catch(val => { + expect(val).to.equal(e); + done(); + }); + }); + }) + + it('should use apiArgs to choose and order the arguments to pass to the API fn', () => { + mkClient({apiArgs: ['parameter', 'command']})({ + command: 'mockCmd', + parameter: 'mockParam', + callback() {} + }); + sinon.assert.calledWith(mockApiFn, 'mockParam', 'mockCmd'); + }); + + it('should not choke on .close()', () => { + mkClient({}).close(); + }) + }) + }) + }) + + describe('postMessage access', () => { + let messenger, win, response; + beforeEach(() => { + response = {}; + messenger = sinon.stub().callsFake((msg) => { + if (msg.mockApiCall) { + win.postMessage({mockApiReturn: {callId: msg.mockApiCall.callId, ...response}}); + } + }); + }); + + function mkClient(options) { + return cmpClient(Object.assign({apiName: 'mockApi'}, options), win); + } + + Object.entries({ + 'on same frame': () => { + win = mockWindow({frames: {mockApiLocator: true}}); + win.addEventListener('message', (evt) => messenger(evt.data)); + }, + 'on parent frame': () => { + win = mockWindow({parent: mockWindow({frames: {mockApiLocator: true}})}) + win.parent.addEventListener('message', evt => messenger(evt.data)) + } + }).forEach(([t, setup]) => { + describe(t, () => { + beforeEach(setup); + + it('should mark client as not direct', () => { + expect(mkClient().isDirect).to.equal(false); + }); + + it('should find and message the CMP frame', () => { + mkClient()({command: 'mockCmd', parameter: 'param'}); + sinon.assert.calledWithMatch(messenger, { + mockApiCall: { + command: 'mockCmd', + parameter: 'param' + } + }) + }); + + it('should use apiArgs to choose what to include in the message payload', () => { + mkClient({apiArgs: ['command']})({ + command: 'cmd', + parameter: 'param' + }); + sinon.assert.calledWithMatch(messenger, sinon.match((arg) => { + return arg.mockApiCall.command === 'cmd' && + !arg.mockApiCall.hasOwnProperty('parameter'); + })) + }); + + it('should not include callback in the payload, but still run it on response', () => { + const cb = sinon.stub(); + mkClient({apiArgs: ['command', 'callback']})({ + command: 'cmd', + callback: cb + }); + sinon.assert.calledWithMatch(messenger, sinon.match(arg => !arg.mockApiCall.hasOwnProperty('callback'))); + sinon.assert.called(cb); + }); + + it('should use callbackArgs to decide what to pass to callback', () => { + const cb = sinon.stub(); + response = {a: 'one', b: 'two'}; + mkClient({callbackArgs: ['a', 'b']})({callback: cb}); + sinon.assert.calledWith(cb, 'one', 'two'); + }) + + describe('should return a promise that', () => { + beforeEach(() => { + response = {returnValue: 'val'} + }) + Object.entries({ + 'callback': [sinon.stub(), 'undefined', undefined], + 'callback, mode = MODE_RETURN': [sinon.stub(), 'undefined', undefined, MODE_RETURN], + 'callback, mode = MODE_CALLBACK': [sinon.stub(), 'undefined', undefined, MODE_CALLBACK], + 'no callback': [undefined, 'response returnValue', 'val'], + 'no callback, mode = MODE_RETURN': [undefined, 'undefined', undefined, MODE_RETURN], + 'no callback, mode = MODE_CALLBACK': [undefined, 'response returnValue', 'val', MODE_CALLBACK], + }).forEach(([t, [callback, tResult, expectedResult, mode]]) => { + describe(`when ${t} is provided`, () => { + Object.entries({ + 'no success flag': {}, + 'with success flag': {success: true} + }).forEach(([t, resp]) => { + it(`resolves to ${tResult} (${t})`, () => { + Object.assign(response, resp); + mkClient({mode})({callback}).then((val) => { + expect(val).to.equal(expectedResult); + }) + }) + }); + + if (mode !== MODE_RETURN) { // in return mode, the promise never rejects + it(`rejects to ${tResult} when success = false`, (done) => { + response.success = false; + mkClient()({mode, callback}).catch((err) => { + expect(err).to.equal(expectedResult); + done(); + }); + }); + } + }) + }); + }); + + describe('messages with same callID', () => { + let callback, callId; + + function runCallback(returnValue) { + win.postMessage({mockApiReturn: {callId, returnValue}}); + } + + beforeEach(() => { + callId = null; + messenger.reset(); + messenger.callsFake((msg) => { + if (msg.mockApiCall) callId = msg.mockApiCall.callId; + }); + callback = sinon.stub(); + }); + + it('should re-use callback for messages with same callId', () => { + mkClient()({callback}); + expect(callId).to.exist; + runCallback('a'); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledWith(callback, 'b'); + }); + + it('should NOT re-use callback if once = true', () => { + mkClient()({callback}, true); + expect(callId).to.exist; + runCallback('a'); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledOnce(callback); + }); + + it('should NOT fire again after .close()', () => { + const client = mkClient(); + client({callback}); + runCallback('a'); + client.close(); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledOnce(callback); + }) + }); + }); + }); + }); +}); diff --git a/test/spec/libraries/currencyUtils_spec.js b/test/spec/libraries/currencyUtils_spec.js new file mode 100644 index 00000000000..9d3d73e6a5f --- /dev/null +++ b/test/spec/libraries/currencyUtils_spec.js @@ -0,0 +1,113 @@ +import {getGlobal} from 'src/prebidGlobal.js'; +import {convertCurrency, currencyCompare, currencyNormalizer} from 'libraries/currencyUtils/currency.js'; + +describe('currency utils', () => { + let sandbox; + before(() => { + if (!getGlobal().convertCurrency) { + getGlobal().convertCurrency = () => null; + getGlobal().convertCurrency.mock = true; + } + }); + + after(() => { + if (getGlobal().convertCurrency.mock) { + delete getGlobal().convertCurrency; + } + }) + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('convertCurrency', () => { + Object.entries({ + 'not available': () => sandbox.stub(getGlobal(), 'convertCurrency').value(undefined), + 'throwing errors': () => sandbox.stub(getGlobal(), 'convertCurrency').callsFake(() => { throw new Error(); }), + }).forEach(([t, setup]) => { + describe(`when currency module is ${t}`, () => { + beforeEach(setup); + + it('should "convert" to the same currency', () => { + expect(convertCurrency(123, 'mock', 'mock', false)).to.eql(123); + }); + + it('should throw when suppressErrors = false', () => { + expect(() => convertCurrency(123, 'c1', 'c2', false)).to.throw(); + }); + + it('should return input value when suppressErrors = true', () => { + expect(convertCurrency(123, 'c1', 'c2', true)).to.eql(123); + }) + }) + }); + + describe('when currency module is working', () => { + beforeEach(() => { + sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amt) => amt * 10) + }); + + it('should be used for actual conversions', () => { + expect(convertCurrency(123, 'c1', 'c2')).to.eql(1230); + sinon.assert.calledWith(getGlobal().convertCurrency, 123, 'c1', 'c2'); + }); + + it('should NOT be used when no conversion is necessary', () => { + expect(convertCurrency(123, 'cur', 'cur')).to.eql(123); + sinon.assert.notCalled(getGlobal().convertCurrency); + }) + }) + }); + + describe('Currency normalization', () => { + let mockConvert; + beforeEach(() => { + mockConvert = sinon.stub().callsFake((amt, from, to) => { + if (from === to) return amt; + return amt / from * to + }) + }); + + describe('currencyNormalizer', () => { + it('converts to toCurrency if set', () => { + const normalize = currencyNormalizer(10, true, mockConvert); + expect(normalize(1, 1)).to.eql(10); + expect(normalize(10, 100)).to.eql(1); + }); + + it('converts to first currency if toCurrency is not set', () => { + const normalize = currencyNormalizer(null, true, mockConvert); + expect(normalize(1, 1)).to.eql(1); + expect(normalize(1, 10)).to.eql(0.1); + }); + + [true, false].forEach(bestEffort => { + it(`passes bestEffort = ${bestEffort} to convert`, () => { + currencyNormalizer(null, bestEffort, mockConvert)(1, 1); + sinon.assert.calledWith(mockConvert, 1, 1, 1, bestEffort); + }) + }) + }); + + describe('currencyCompare', () => { + let compare + beforeEach(() => { + compare = currencyCompare((val) => [val.amount, val.cur], currencyNormalizer(null, false, mockConvert)) + }); + [ + [{amount: 1, cur: 1}, {amount: 1, cur: 10}, 1], + [{amount: 10, cur: 1}, {amount: 0.1, cur: 100}, 1], + [{amount: 1, cur: 1}, {amount: 10, cur: 10}, 0], + ].forEach(([a, b, expected]) => { + it(`should compare ${a.amount}/${a.cur} and ${b.amount}/${b.cur}`, () => { + expect(compare(a, b)).to.equal(expected); + expect(compare(b, a)).to.equal(-expected); + }); + }); + }) + }) +}) diff --git a/test/spec/libraries/mspa/activityControls_spec.js b/test/spec/libraries/mspa/activityControls_spec.js new file mode 100644 index 00000000000..f232dc2563f --- /dev/null +++ b/test/spec/libraries/mspa/activityControls_spec.js @@ -0,0 +1,256 @@ +import {mspaRule, setupRules, isTransmitUfpdConsentDenied, isTransmitGeoConsentDenied, isBasicConsentDenied, sensitiveNoticeIs, isConsentDenied} from '../../../../libraries/mspa/activityControls.js'; +import {ruleRegistry} from '../../../../src/activities/rules.js'; + +describe('Consent interpretation', () => { + function mkConsent(flags) { + return Object.assign({ + // not covered, opt in to targeted, sale, and share, all notices given, opt into precise geo + Gpc: 0, + KnownChildSensitiveDataConsents: [0, 0], + MspaCoveredTransaction: 2, + MspaOptOutOptionMode: 0, + MspaServiceProviderMode: 0, + PersonalDataConsents: 0, + SaleOptOut: 2, + SaleOptOutNotice: 1, + SensitiveDataLimitUseNotice: 1, + SensitiveDataProcessing: [0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0], + SensitiveDataProcessingOptOutNotice: 1, + SharingNotice: 1, + SharingOptOut: 2, + SharingOptOutNotice: 1, + TargetedAdvertisingOptOut: 2, + TargetedAdvertisingOptOutNotice: 1, + Version: 1 + }, flags) + } + describe('isBasicConsentDenied', () => { + it('should be false (basic consent conditions pass) with variety of notice and opt in', () => { + const result = isBasicConsentDenied(mkConsent()); + expect(result).to.equal(false); + }); + it('should be true (basic consent conditions do not pass) with personal data consent set to true (invalid state)', () => { + const result = isBasicConsentDenied(mkConsent({ + PersonalDataConsents: 2 + })); + expect(result).to.equal(true); + }); + it('should be true (basic consent conditions do not pass) with covered set to zero (invalid state)', () => { + const result = isBasicConsentDenied(mkConsent({ + MspaCoveredTransaction: 0 + })); + expect(result).to.equal(true); + }); + it('should not deny when consent for under-13 is null', () => { + expect(isBasicConsentDenied(mkConsent({ + KnownChildSensitiveDataConsents: [0, null] + }))).to.be.false; + }) + }); + + describe('isConsentDenied', () => { + it('should be false (consent given personalized ads / sale / share) with variety of notice and opt in', () => { + const result = isConsentDenied(mkConsent()); + expect(result).to.equal(false); + }); + it('should be true (no consent) on opt out of targeted ads via TargetedAdvertisingOptOut', () => { + const result = isConsentDenied(mkConsent({ + TargetedAdvertisingOptOut: 1 + })); + expect(result).to.equal(true); + }); + it('should be true (no consent) on opt out of targeted ads via no TargetedAdvertisingOptOutNotice', () => { + const result = isConsentDenied(mkConsent({ + TargetedAdvertisingOptOutNotice: 2 + })); + expect(result).to.equal(true); + }); + it('should be true (no consent) if TargetedAdvertisingOptOutNotice is 0 and TargetedAdvertisingOptOut is 2', () => { + const result = isConsentDenied(mkConsent({ + TargetedAdvertisingOptOutNotice: 0 + })); + expect(result).to.equal(true); + }); + it('requires also SharingNotice to accept opt-in for Sharing', () => { + expect(isConsentDenied(mkConsent({ + SharingNotice: 0 + }))).to.be.true; + }) + }); + + describe('isTransmitUfpdConsentDenied', () => { + it('should be false (consent given to add ufpd) with variety of notice and opt in', () => { + const result = isTransmitUfpdConsentDenied(mkConsent()); + expect(result).to.equal(false); + }); + Object.entries({ + 'health information': 2, + 'biometric data': 6, + }).forEach(([t, flagNo]) => { + it(`'should be true (consent denied to add ufpd) if no consent to process ${t}'`, () => { + const consent = mkConsent(); + consent.SensitiveDataProcessing[flagNo] = 1; + expect(isTransmitUfpdConsentDenied(consent)).to.be.true; + }) + }); + + ['SharingNotice', 'SensitiveDataLimitUseNotice'].forEach(flag => { + it(`should be true (consent denied to add ufpd) without ${flag}`, () => { + expect(isTransmitUfpdConsentDenied(mkConsent({ + [flag]: 2 + }))).to.be.true; + }) + }); + + ['SaleOptOut', 'TargetedAdvertisingOptOut'].forEach(flag => { + it(`should be true (consent denied to add ufpd) with ${flag}`, () => { + expect(isTransmitUfpdConsentDenied(mkConsent({ + [flag]: 1 + }))).to.be.true; + }) + }); + + it('should be true (basic consent conditions do not pass) with sensitive opt in but no notice', () => { + const cd = mkConsent({ + SensitiveDataLimitUseNotice: 0 + }); + cd.SensitiveDataProcessing[0] = 2; + expect(isTransmitUfpdConsentDenied(cd)).to.be.true; + }); + + it('should deny when sensitive notice is missing', () => { + const result = isTransmitUfpdConsentDenied(mkConsent({ + SensitiveDataLimitUseNotice: 2 + })); + expect(result).to.equal(true); + }); + + it('should not deny when biometric data opt-out is null', () => { + const cd = mkConsent(); + cd.SensitiveDataProcessing[6] = null; + expect(isTransmitUfpdConsentDenied(cd)).to.be.false; + }) + }); + + describe('isTransmitGeoConsentDenied', () => { + function geoConsent(geoOptOut, flags) { + const consent = mkConsent(flags); + consent.SensitiveDataProcessing[7] = geoOptOut; + return consent; + } + it('should be true (consent denied to add precise geo) -- sensitive flag denied', () => { + const result = isTransmitGeoConsentDenied(geoConsent(1)); + expect(result).to.equal(true); + }); + it('should be true (consent denied to add precise geo) -- sensitive data limit usage not given', () => { + const result = isTransmitGeoConsentDenied(geoConsent(1, { + SensitiveDataLimitUseNotice: 0 + })); + expect(result).to.equal(true); + }); + it('should be false (consent given to add precise geo) -- sensitive position 8 (index 7) is true', () => { + const result = isTransmitGeoConsentDenied(geoConsent(2)); + expect(result).to.equal(false); + }); + }) +}); + +describe('mspaRule', () => { + it('does not apply if SID is not applicable', () => { + const rule = mspaRule([1, 2], () => null, () => true, () => [3, 4]); + expect(rule()).to.not.exist; + }); + + it('does not apply when no SID is applicable', () => { + const rule = mspaRule([1], () => null, () => true, () => []); + expect(rule()).to.not.exist; + }); + + describe('when SID is applicable', () => { + let consent, denies; + function mkRule() { + return mspaRule([1, 2], () => consent, denies, () => [2]) + } + + beforeEach(() => { + consent = null; + denies = sinon.stub(); + }); + + it('should deny when no consent is available', () => { + expect(mkRule()().allow).to.equal(false); + }); + + Object.entries({ + 'denies': true, + 'allows': false + }).forEach(([t, denied]) => { + it(`should check if deny fn ${t}`, () => { + denies.returns(denied); + consent = {mock: 'value'}; + const result = mkRule()(); + sinon.assert.calledWith(denies, consent); + if (denied) { + expect(result.allow).to.equal(false); + } else { + expect(result).to.not.exist; + } + }) + }) + }) +}); + +describe('setupRules', () => { + let rules, registerRule, isAllowed, consent; + beforeEach(() => { + rules = { + mockActivity: sinon.stub().returns(true) + }; + ([registerRule, isAllowed] = ruleRegistry()); + consent = { + applicableSections: [1], + parsedSections: { + mockApi: [ + { + mock: 'consent' + } + ] + } + }; + }); + + function runSetup(api, sids, normalize) { + return setupRules(api, sids, normalize, rules, registerRule, () => consent) + } + + it('should use flatten section data for the given api', () => { + runSetup('mockApi', [1]); + expect(isAllowed('mockActivity', {})).to.equal(false); + sinon.assert.calledWith(rules.mockActivity, {mock: 'consent'}) + }); + + it('should not choke when no consent data is available', () => { + consent = null; + runSetup('mockApi', [1]); + expect(isAllowed('mockActivity', {})).to.equal(true); + }); + + it('should check applicableSections against given SIDs', () => { + runSetup('mockApi', [2]); + expect(isAllowed('mockActivity', {})).to.equal(true); + }); + + it('should pass flattened consent through normalizeConsent', () => { + const normalize = sinon.stub().returns({normalized: 'consent'}) + runSetup('mockApi', [1], normalize); + expect(isAllowed('mockActivity', {})).to.equal(false); + sinon.assert.calledWith(normalize, {mock: 'consent'}); + sinon.assert.calledWith(rules.mockActivity, {normalized: 'consent'}); + }); + + it('should return a function that unregisters activity controls', () => { + const dereg = runSetup('mockApi', [1]); + dereg(); + expect(isAllowed('mockActivity', {})).to.equal(true); + }); +}) diff --git a/test/spec/libraries/sizeUtils_spec.js b/test/spec/libraries/sizeUtils_spec.js new file mode 100644 index 00000000000..1c954c6accf --- /dev/null +++ b/test/spec/libraries/sizeUtils_spec.js @@ -0,0 +1,30 @@ +import {getAdUnitSizes} from '../../../libraries/sizeUtils/sizeUtils.js'; +import {expect} from 'chai/index.js'; + +describe('getAdUnitSizes', function () { + it('returns an empty response when adUnits is undefined', function () { + let sizes = getAdUnitSizes(); + expect(sizes).to.be.undefined; + }); + + it('returns an empty array when invalid data is present in adUnit object', function () { + let sizes = getAdUnitSizes({sizes: 300}); + expect(sizes).to.deep.equal([]); + }); + + it('retuns an array of arrays when reading from adUnit.sizes', function () { + let sizes = getAdUnitSizes({sizes: [300, 250]}); + expect(sizes).to.deep.equal([[300, 250]]); + + sizes = getAdUnitSizes({sizes: [[300, 250], [300, 600]]}); + expect(sizes).to.deep.equal([[300, 250], [300, 600]]); + }); + + it('returns an array of arrays when reading from adUnit.mediaTypes.banner.sizes', function () { + let sizes = getAdUnitSizes({mediaTypes: {banner: {sizes: [300, 250]}}}); + expect(sizes).to.deep.equal([[300, 250]]); + + sizes = getAdUnitSizes({mediaTypes: {banner: {sizes: [[300, 250], [300, 600]]}}}); + expect(sizes).to.deep.equal([[300, 250], [300, 600]]); + }); +}); diff --git a/test/spec/libraries/urlUtils_spec.js b/test/spec/libraries/urlUtils_spec.js new file mode 100644 index 00000000000..9dd66b05407 --- /dev/null +++ b/test/spec/libraries/urlUtils_spec.js @@ -0,0 +1,24 @@ +import {tryAppendQueryString} from '../../../libraries/urlUtils/urlUtils.js'; +import assert from 'assert'; + +describe('tryAppendQueryString', function () { + it('should append query string to existing url', function () { + var url = 'www.a.com?'; + var key = 'b'; + var value = 'c'; + + var output = tryAppendQueryString(url, key, value); + + var expectedResult = url + key + '=' + encodeURIComponent(value) + '&'; + assert.equal(output, expectedResult); + }); + + it('should return existing url, if the value is empty', function () { + var url = 'www.a.com?'; + var key = 'b'; + var value = ''; + + var output = tryAppendQueryString(url, key, value); + assert.equal(output, url); + }); +}); diff --git a/test/spec/modules/33acrossAnalyticsAdapter_spec.js b/test/spec/modules/33acrossAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..9e0d928cd97 --- /dev/null +++ b/test/spec/modules/33acrossAnalyticsAdapter_spec.js @@ -0,0 +1,1163 @@ +// @ts-nocheck +import analyticsAdapter from 'modules/33acrossAnalyticsAdapter.js'; +import { log } from 'modules/33acrossAnalyticsAdapter.js'; +import * as mockGpt from 'test/spec/integration/faker/googletag.js'; +import * as events from 'src/events.js'; +import * as faker from 'faker'; +import CONSTANTS from 'src/constants.json'; +import { gdprDataHandler, gppDataHandler, uspDataHandler } from '../../../src/adapterManager'; +import { DEFAULT_ENDPOINT, POST_GAM_TIMEOUT, locals } from '../../../modules/33acrossAnalyticsAdapter'; +const { EVENTS, BID_STATUS } = CONSTANTS; + +describe('33acrossAnalyticsAdapter:', function () { + let sandbox; + let assert = getLocalAssert(); + + beforeEach(function () { + mockGpt.reset(); + + sandbox = sinon.createSandbox({ + useFakeTimers: { + now: new Date(2023, 3, 3, 0, 1, 33, 425), + }, + }); + + sandbox.stub(events, 'getEvents').returns([]); + + sandbox.spy(log, 'info'); + sandbox.spy(log, 'warn'); + sandbox.spy(log, 'error'); + + sandbox.stub(navigator, 'sendBeacon').callsFake(function (url, data) { + const json = JSON.parse(data); + assert.isValidAnalyticsReport(json); + + return true; + }); + }); + + afterEach(function () { + analyticsAdapter.disableAnalytics(); + mockGpt.enable(); + sandbox.restore(); + }); + + describe('enableAnalytics:', function () { + context('When pid is given', function () { + context('but endpoint is not', function () { + it('uses the default endpoint', function () { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + }, + }); + + assert.equal(analyticsAdapter.getUrl(), DEFAULT_ENDPOINT); + }); + }); + + context('but the endpoint is invalid', function () { + it('logs an info message', function () { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + endpoint: 'foo' + }, + }); + + assert.calledWithExactly(log.info, 'Invalid endpoint provided for "options.endpoint". Using default endpoint.'); + }); + }); + }); + + context('When endpoint is given', function () { + context('but pid is not', function () { + it('logs an error message', function () { + analyticsAdapter.enableAnalytics({ + options: { + endpoint: faker.internet.url() + }, + }); + + assert.calledWithExactly(log.error, 'No partnerId provided for "options.pid". No analytics will be sent.'); + }); + }); + }); + + context('When pid and endpoint are given', function () { + context('and an invalid timeout config value is given', function () { + it('logs an info message', function () { + [null, 'foo', -1].forEach(timeout => { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + endpoint: 'http://test-endpoint', + timeout + }, + }); + analyticsAdapter.disableAnalytics(); + + assert.calledWithExactly(log.info, 'Invalid timeout provided for "options.timeout". Using default timeout of 10000ms.'); + log.info.resetHistory(); + }); + }); + }); + }); + }); + + // check that upcoming tests are derived from a valid report + describe('Report Mocks', function () { + it('the report should have the correct format', function () { + assert.isValidAnalyticsReport(createReportWithThreeBidWonEvents()); + }); + }); + + describe('Event Handling', function () { + beforeEach(function () { + this.defaultTimeout = 10000; + this.enableAnalytics = (options) => { + analyticsAdapter.enableAnalytics({ + options: { + endpoint: 'http://test-endpoint', + pid: 'test-pid', + timeout: this.defaultTimeout, + ...options + }, + }); + window.googletag.cmd.forEach(cmd => cmd()); + } + }); + + context('when an auction is complete', function () { + context('and the AnalyticsReport is sent successfully to the given endpoint', function () { + it('calls "sendBeacon" with all won bids', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const [url, jsonString] = navigator.sendBeacon.firstCall.args; + const { auctions } = JSON.parse(jsonString); + + assert.lengthOf(mapToBids(auctions).filter(bid => bid.hasWon), 3); + }); + + it('calls "sendBeacon" with the correct report string', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + + it('logs an info message containing the report', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())) + .returns(true); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.info, `Analytics report sent to ${endpoint}`, createReportWithThreeBidWonEvents()); + }); + + it('calls "sendBeacon" as soon as all values are available (before timeout)', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + }); + + context('and a valid US Privacy configuration is present', function () { + ['1YNY', '1---', '1NY-', '1Y--', '1--Y', '1N--', '1--N', '1NNN'].forEach(consent => { + it(`calls "sendBeacon" with a report containing the "${consent}" privacy string`, function () { + sandbox.stub(uspDataHandler, 'getConsentData').returns(consent); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + usPrivacy: consent + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + }); + + context('and a GDPR Privacy configuration is present', function () { + it('it calls "sendBeacon" with a report containing the GDPR consent string', function () { + sandbox.stub(gdprDataHandler, 'getConsentData').returns({ + consentString: 'foo', + gdprApplies: true + }); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + gdpr: 1, + gdprConsent: 'foo' + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + }); + + context('when an auction is complete and a GPP configuration is present', function () { + it('it calls "sendBeacon" with a report containing the GPP consent string', function () { + sandbox.stub(gppDataHandler, 'getConsentData').returns({ + gppString: 'gppString', + applicableSections: [7] + }); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + gpp: 'gppString', + gppSid: [7] + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + + context('when an error occurs while sending the AnalyticsReport', function () { + it('logs an error', function () { + this.enableAnalytics(); + navigator.sendBeacon.returns(false); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.error, 'Analytics report exceeded User-Agent data limits and was not sent.', createReportWithThreeBidWonEvents()); + }); + }); + + context('when an auction report was already sent', function () { + context('and a new bid won event is returned after the report completes', function () { + it('finishes the auction without error', function () { + const incompleteAnalyticsReport = createReportWithThreeBidWonEvents(); + incompleteAnalyticsReport.auctions.forEach(auction => { + auction.adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + delete bid.bidResponse; + bid.hasWon = 0; + bid.status = 'pending'; + }); + }); + }); + + this.enableAnalytics(); + const { prebid: [auction] } = getMockEvents(); + + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + + sandbox.clock.tick(this.defaultTimeout + 1000); + + for (let bidResponseEvent of auction.BID_RESPONSE) { + events.emit(EVENTS.BID_RESPONSE, bidResponseEvent); + }; + for (let bidWonEvent of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWonEvent); + }; + + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + + sandbox.clock.tick(1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', incompleteAnalyticsReport); + }); + }); + + context('and another auction completes after that', function () { + it('sends the new report', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledTwice(navigator.sendBeacon); + }); + }); + }); + + context('when two auctions overlap', function() { + it('sends a report for each auction', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledTwice(navigator.sendBeacon); + }); + }); + + context('when an AUCTION_END event is received before BID_WON events', function () { + it('sends a report with the bids that have won after all bids are won', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + const { prebid: [auction] } = getMockEvents(); + + performStandardAuction({ exclude: [EVENTS.BID_WON] }); + + assert.notCalled(navigator.sendBeacon); + for (let bidWon of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWon); + } + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + }); + + context('when a BID_WON event is received', function () { + context('and there is no record of that bid being requested', function () { + it('logs a warning message', function () { + this.enableAnalytics(); + + const mockEvents = getMockEvents(); + const { prebid } = mockEvents; + const [auction] = prebid; + + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + + const fakeBidWonEvent = Object.assign(auction.BID_WON[0], { + transactionId: 'foo' + }) + + events.emit(EVENTS.BID_WON, fakeBidWonEvent); + + const { auctionId, requestId } = fakeBidWonEvent; + assert.calledWithExactly(log.error, `Cannot find bid "${requestId}" in auction "${auctionId}".`); + }); + }); + }); + + context('when a BID_REJECTED event is received', function () { + it(`marks the rejected bid as "rejected"`, function () { + this.enableAnalytics(); + + const auction = getMockEvents().prebid[0]; + + // Start the auction + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + + // Reject first bid + const bidToReject = auction.BID_REQUESTED[0].bids[0]; + events.emit(EVENTS.BID_REJECTED, auction.BID_REJECTED[0]); + + // Accept remaining bids + for (let i = 1; i < auction.BID_RESPONSE.length; ++i) { + events.emit(EVENTS.BID_RESPONSE, auction.BID_RESPONSE[i]); + }; + + // Complete the auction + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + + sandbox.clock.tick(this.defaultTimeout + 1); + + // Verify that we detected that the first bid was rejected + const expectedRejectedBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[0].bids[0]; + assert.strictEqual(expectedRejectedBid.status, 'rejected'); + }); + }); + + context('when a transaction does not reach its complete state', function () { + context('and a timeout config value has been given', function () { + context('and the timeout value has elapsed', function () { + it('logs a warning', function () { + const timeout = 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + + sandbox.clock.tick(timeout + 1000); + + assert.calledWithExactly(log.warn, 'Timed out waiting for ad transactions to complete. Sending report.'); + }); + + it(`marks timed out bids as "timeout"`, function () { + const timeout = 2000; + this.enableAnalytics({ timeout }); + const request = getMockEvents().prebid[0].BID_REQUESTED[0]; + const bidToTimeout = request.bids[0]; + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + sandbox.clock.tick(1); + events.emit(EVENTS.BID_TIMEOUT, [{ + auctionId: request.auctionId, + bidId: bidToTimeout.bidId, + transactionId: bidToTimeout.transactionId, + }]); + sandbox.clock.tick(timeout + 1000); + + const timeoutBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[0].bids[0]; + assert.strictEqual(timeoutBid.status, 'timeout'); + }); + }); + }); + + context('and a timeout config value has not been given', function () { + context('and the default timeout has elapsed', function () { + it('logs an error', function () { + this.enableAnalytics(); + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.warn, 'Timed out waiting for ad transactions to complete. Sending report.'); + }); + }) + }); + + context('and the `slotRenderEnded` event fired for all bids, but not all bids have won', function () { + context('and the GAM slot IDs are configured as the ad unit codes', function () { + it('sends a report after the all `slotRenderEnded` events have fired and timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 1); + }); + }); + + context('and the slot element IDs are configured as the ad unit codes', function () { + it('sends a report after the all `slotRenderEnded` events have fired and timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd'], useSlotElementIds: true}); + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 1); + }); + }); + + it('does NOT send a report if not all `slotRenderEnded` events have timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(POST_GAM_TIMEOUT - 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 0); + }); + }); + + context('and the `slotRenderEnded` event has fired for an unknown slot code', function () { + it('logs a warning message', function () { + this.enableAnalytics(); + + const { prebid: [auction], gam } = getMockEvents(); + auction.AUCTION_INIT.adUnits[0].code = 'INVALID_AD_UNIT_CODE'; + + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.calledWithExactly(log.warn, + 'Could not find configured ad unit matching GAM render of slot:', + { slotName: `${adUnitCodes[0]} - ${adSlotElementIds[0]}` }); + }); + }); + + context('and the incomplete report has been sent successfully', function () { + it('sends a report string with any bids with rendered status set to hasWon: 1', function () { + navigator.sendBeacon.returns(true); + + this.enableAnalytics(); + + performStandardAuction({exclude: ['auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const incompleteSentBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[1].bids[0]; + assert.strictEqual(incompleteSentBid.hasWon, 1); + }); + + it('reports bids with only targetingSet status as hasWon: 0', function () { + navigator.sendBeacon.returns(true); + + this.enableAnalytics(); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const incompleteSentBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[1].bids[0]; + assert.strictEqual(incompleteSentBid.hasWon, 0); + }); + + it('logs an info message', function () { + navigator.sendBeacon.returns(true); + + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWith(log.info, `Analytics report sent to ${endpoint}`); + }); + }); + }); + + context('when the transaction manager has open transactions', function () { + it('reports those transactions as pending', function () { + this.enableAnalytics(); + + const { prebid: [auction] } = getMockEvents(); + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.equal(manager.status().pending.length, auction.BID_REQUESTED[0].bids.length); + }); + + context('and a single bidWon event has triggered', function () { + it('completes the transaction', function () { + this.enableAnalytics(); + + const { prebid: [auction] } = getMockEvents(); + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + events.emit(EVENTS.BID_WON, auction.BID_WON[0]); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 1, + pending: auction.BID_REQUESTED[0].bids.length - 1 + }); + }); + }); + + context('and a single slotRenderEnded event has triggered', function () { + context('and the Google Ad Manager timeout has not elapsed', function () { + it('does NOT complete the transaction', function () { + this.enableAnalytics(); + + const { prebid: [auction], gam } = getMockEvents(); + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 0, + pending: auction.BID_REQUESTED[0].bids.length + }); + }); + }); + + context('and the Google Ad Manager timeout has elapsed', function () { + it('completes the transaction', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({timeout}); + + const { prebid: [auction], gam } = getMockEvents(); + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 1, + pending: auction.BID_REQUESTED[0].bids.length - 1 + }); + }); + }); + }); + }); + }); +}); + +const adUnitCodes = ['/19968336/header-bid-tag-0', '/19968336/header-bid-tag-1', '/17118521/header-bid-tag-2']; +const adSlotElementIds = ['ad-slot-div-0', 'ad-slot-div-1', 'ad-slot-div-2']; + +function performStandardAuction({ exclude = [], useSlotElementIds = false } = {}) { + const mockEvents = getMockEvents(); + const { prebid, gam } = mockEvents; + const [auction] = prebid; + + if (!exclude.includes(EVENTS.AUCTION_INIT)) { + if (useSlotElementIds) { + // With this option, identify the ad units by slot element IDs instead of GAM paths + auction.AUCTION_INIT.adUnits.forEach((adUnit, i) => { + adUnit.code = adSlotElementIds[i]; + }); + } + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + } + + if (!exclude.includes(EVENTS.BID_REQUESTED)) { + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + } + + if (!exclude.includes(EVENTS.BID_RESPONSE)) { + for (let bidResponseEvent of auction.BID_RESPONSE) { + events.emit(EVENTS.BID_RESPONSE, bidResponseEvent); + }; + } + + if (!exclude.includes(EVENTS.AUCTION_END)) { + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + } + + if (!exclude.includes('slotRenderEnded')) { + for (let gEvent of gam.slotRenderEnded) { + mockGpt.emitEvent('slotRenderEnded', gEvent); + } + } + + if (!exclude.includes(EVENTS.BID_WON)) { + for (let bidWonEvent of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWonEvent); + }; + } +} + +function mapToBids(auctions) { + return auctions.flatMap( + auction => auction.adUnits.flatMap( + au => au.bids + ) + ); +} + +function getLocalAssert() { + function isValidAnalyticsReport(report) { + assert.containsAllKeys(report, ['analyticsVersion', 'pid', 'src', 'pbjsVersion', 'auctions']); + if ('usPrivacy' in report) { + assert.match(report.usPrivacy, /[0|1][Y|N|-]{3}/); + } + if ('gdpr' in report) { + assert.oneOf(report.gdpr, [0, 1]); + } + if (report.gdpr === 1) { + assert.isString(report.gdprConsent); + } + if ('gpp' in report) { + assert.isString(report.gpp); + assert.isArray(report.gppSid); + } + if ('coppa' in report) { + assert.oneOf(report.coppa, [0, 1]); + } + + assert.equal(report.analyticsVersion, '1.0.0'); + assert.isString(report.pid); + assert.isString(report.src); + assert.equal(report.pbjsVersion, '$prebid.version$'); + assert.isArray(report.auctions); + assert.isAbove(report.auctions.length, 0); + report.auctions.forEach(isValidAuction); + } + function isValidAuction(auction) { + assert.hasAllKeys(auction, ['adUnits', 'auctionId', 'userIds']); + assert.isArray(auction.adUnits); + assert.isString(auction.auctionId); + assert.isArray(auction.userIds); + auction.adUnits.forEach(isValidAdUnit); + } + function isValidAdUnit(adUnit) { + assert.hasAllKeys(adUnit, ['transactionId', 'adUnitCode', 'slotId', 'mediaTypes', 'sizes', 'bids']); + assert.isString(adUnit.transactionId); + assert.isString(adUnit.adUnitCode); + assert.isString(adUnit.slotId); + assert.isArray(adUnit.mediaTypes); + assert.isArray(adUnit.sizes); + assert.isArray(adUnit.bids); + adUnit.mediaTypes.forEach(isValidMediaType); + adUnit.sizes.forEach(isValidSizeString); + adUnit.bids.forEach(isValidBid); + } + function isValidBid(bid) { + assert.containsAllKeys(bid, ['bidder', 'bidId', 'source', 'status', 'hasWon']); + if ('bidResponse' in bid) { + isValidBidResponse(bid.bidResponse); + } + assert.isString(bid.bidder); + assert.isString(bid.bidId); + assert.isString(bid.source); + assert.oneOf(bid.status, ['pending', 'timeout', 'targetingSet', 'rendered', 'success', 'rejected', 'no-bid', 'error']); + assert.oneOf(bid.hasWon, [0, 1]); + } + function isValidBidResponse(bidResponse) { + assert.containsAllKeys(bidResponse, ['mediaType', 'size', 'cur', 'cpm', 'cpmFloor']); + if ('cpmOrig' in bidResponse) { + assert.isNumber(bidResponse.cpmOrig); + } + isValidMediaType(bidResponse.mediaType); + isValidSizeString(bidResponse.size); + assert.isString(bidResponse.cur); + assert.isNumber(bidResponse.cpm); + assert.isNumber(bidResponse.cpmFloor); + } + function isValidMediaType(mediaType) { + assert.oneOf(mediaType, ['banner', 'video', 'native']); + } + function isValidSizeString(size) { + assert.match(size, /[0-9]+x[0-9]+/); + } + + function calledOnceWithStringJsonEquivalent(sinonSpy, ...args) { + sinon.assert.calledOnce(sinonSpy); + args.forEach((arg, i) => { + const stubCallArgs = sinonSpy.firstCall.args[i] + + if (typeof arg === 'object') { + assert.deepEqual(JSON.parse(stubCallArgs), arg); + } else { + assert.strictEqual(stubCallArgs, arg); + } + }); + } + + sinon.assert.expose(assert, { prefix: '' }); + return { + ...assert, + calledOnceWithStringJsonEquivalent, + isValidAnalyticsReport, + isValidAuction, + isValidAdUnit, + isValidBid, + isValidBidResponse, + isValidMediaType, + isValidSizeString, + } +}; + +function createReportWithThreeBidWonEvents() { + return { + pid: 'test-pid', + src: 'pbjs', + analyticsVersion: '1.0.0', + pbjsVersion: '$prebid.version$', + auctions: [{ + adUnits: [{ + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + adUnitCode: adUnitCodes[0], + slotId: adUnitCodes[0], + mediaTypes: ['banner'], + sizes: ['300x250', '300x600'], + bids: [{ + bidder: 'bidder0', + bidId: '20661fc5fbb5d9b', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '300x250' + }, + hasWon: 1 + }] + }, { + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + adUnitCode: adUnitCodes[1], + slotId: adUnitCodes[1], + mediaTypes: ['banner'], + sizes: ['728x90', '970x250'], + bids: [{ + bidder: 'bidder0', + bidId: '21ad295f40dd7ab', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '728x90' + }, + hasWon: 1 + }] + }, { + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + adUnitCode: adUnitCodes[2], + slotId: adUnitCodes[2], + mediaTypes: ['banner'], + sizes: ['300x250'], + bids: [{ + bidder: 'bidder0', + bidId: '22108ac7b778717', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '728x90' + }, + hasWon: 1 + }] + }], + auctionId: 'auction-000', + userIds: ['33acrossId'] + }], + }; +} + +function getMockEvents() { + const auctionId = 'auction-000'; + const userId = { + '33acrossId': { + envelope: 'v1.0014', + }, + }; + + return { + gam: { + slotRenderEnded: [ + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[0], divId: adSlotElementIds[0] }), + isEmpty: true, + slotContentChanged: true, + size: null, + advertiserId: null, + campaignId: null, + creativeId: null, + creativeTemplateId: null, + labelIds: null, + lineItemId: null, + isBackfill: false, + }, + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[1], divId: adSlotElementIds[1] }), + isEmpty: false, + slotContentChanged: true, + size: [1, 1], + advertiserId: 12345, + campaignId: 400000001, + creativeId: 6789, + creativeTemplateId: null, + labelIds: null, + lineItemId: 1011, + isBackfill: false, + yieldGroupIds: null, + companyIds: null, + }, + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[2], divId: adSlotElementIds[2] }), + isEmpty: false, + slotContentChanged: true, + size: [728, 90], + advertiserId: 12346, + campaignId: 299999000, + creativeId: 6790, + creativeTemplateId: null, + labelIds: null, + lineItemId: 1012, + isBackfill: false, + yieldGroupIds: null, + companyIds: null, + }, + ], + }, + prebid: [{ + AUCTION_INIT: { + auctionId, + adUnits: [ + { + code: adUnitCodes[0], + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bids: [ + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [ + [300, 250], + [300, 600], + ], + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + ortb2Imp: { + ext: { + gpid: adUnitCodes[0], + }, + }, + }, + { + code: adUnitCodes[1], + mediaTypes: { + banner: { + sizes: [ + [728, 90], + [970, 250], + ], + }, + }, + bids: [ + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [ + [728, 90], + [970, 250], + ], + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + ortb2Imp: { + ext: { + gpid: adUnitCodes[1], + }, + }, + }, + { + code: adUnitCodes[2], + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: '33across', + userId, + }, + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [[300, 250]], + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + ortb2Imp: { + ext: { + gpid: adUnitCodes[2], + }, + }, + }, + ], + bidderRequests: [ + { + bids: [ + { userId }, + ], + } + ], + }, + BID_REQUESTED: [ + { + auctionId, + bids: [ + { + bidder: 'bidder0', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + bidId: '20661fc5fbb5d9b', + src: 'client', + }, + { + bidder: 'bidder0', + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + bidId: '21ad295f40dd7ab', + src: 'client', + }, + { + bidder: 'bidder0', + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + bidId: '22108ac7b778717', + src: 'client', + }, + ], + }], + BID_RESPONSE: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + size: '300x250', + source: 'client', + status: 'targetingSet' + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '21ad295f40dd7ab', + size: '728x90', + source: 'client', + status: 'targetingSet', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '22108ac7b778717', + size: '728x90', + source: 'client', + status: 'targetingSet', + }], + BID_WON: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + size: '300x250', + source: 'client', + status: 'rendered', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '21ad295f40dd7ab', + size: '728x90', + source: 'client', + status: 'rendered', + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '22108ac7b778717', + size: '728x90', + source: 'client', + status: 'rendered', + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + }], + BID_REJECTED: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 2 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + width: 300, + height: 250, + source: 'client', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + statusMessage: 'Bid available', + rejectionReason: 'Bid does not meet price floor', + }], + AUCTION_END: { + auctionId, + }, + }], + }; +} diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index aa2621f1fa0..9cc038428bc 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -200,6 +200,29 @@ describe('33acrossBidAdapter:', function () { return this; }; + this.withCoppa = coppaValue => { + Object.assign(ttxRequest.regs, { + coppa: coppaValue + }); + + return this; + }; + + this.withGppConsent = (consentString, applicableSections) => { + Object.assign(ttxRequest, { + regs: { + gpp: consentString, + gpp_sid: applicableSections, + ext: Object.assign( + {}, + ttxRequest.regs.ext + ) + } + }); + + return this; + }; + this.withSite = site => { Object.assign(ttxRequest, { site }); return this; @@ -1049,11 +1072,11 @@ describe('33acrossBidAdapter:', function () { }); it('returns corresponding test server requests with gdpr consent data', function() { - sandbox.stub(config, 'getConfig').callsFake(() => { - return { + sandbox.stub(config, 'getConfig') + .withArgs('ttxSettings') + .returns({ 'url': 'https://foo.com/hb/' - } - }); + }); const ttxRequest = new TtxRequestBuilder() .withBanner() @@ -1085,11 +1108,11 @@ describe('33acrossBidAdapter:', function () { }); it('returns corresponding test server requests with default gdpr consent data', function() { - sandbox.stub(config, 'getConfig').callsFake(() => { - return { + sandbox.stub(config, 'getConfig') + .withArgs('ttxSettings') + .returns({ 'url': 'https://foo.com/hb/' - } - }); + }); const ttxRequest = new TtxRequestBuilder() .withBanner() @@ -1128,11 +1151,11 @@ describe('33acrossBidAdapter:', function () { }); it('returns corresponding test server requests with us_privacy consent data', function() { - sandbox.stub(config, 'getConfig').callsFake(() => { - return { + sandbox.stub(config, 'getConfig') + .withArgs('ttxSettings') + .returns({ 'url': 'https://foo.com/hb/' - } - }); + }); const ttxRequest = new TtxRequestBuilder() .withBanner() @@ -1164,15 +1187,102 @@ describe('33acrossBidAdapter:', function () { }); it('returns corresponding test server requests with default us_privacy consent data', function() { - sandbox.stub(config, 'getConfig').callsFake(() => { - return { + sandbox.stub(config, 'getConfig') + .withArgs('ttxSettings') + .returns({ 'url': 'https://foo.com/hb/' + }); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .withUrl('https://foo.com/hb/') + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + + context('when coppa has been enabled', function() { + beforeEach(function() { + sandbox.stub(config, 'getConfig').withArgs('coppa').returns(true); + }); + + it('returns corresponding server requests with coppa: 1', function() { + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() + .withCoppa(1) + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + + context('when coppa has been disabled', function() { + beforeEach(function() { + sandbox.stub(config, 'getConfig').withArgs('coppa').returns(false); + }); + + it('returns corresponding server requests with coppa: 0', function() { + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() + .withCoppa(0) + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + + context('when GPP consent data exists', function() { + beforeEach(function() { + bidderRequest = { + ...bidderRequest, + gppConsent: { + gppString: 'foo', + applicableSections: '123' } - }); + } + }); + it('returns corresponding server requests with GPP consent data', function() { const ttxRequest = new TtxRequestBuilder() .withBanner() .withProduct() + .withGppConsent('foo', '123') + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + + it('returns corresponding test server requests with GPP consent data', function() { + sandbox.stub(config, 'getConfig').withArgs('ttxSettings') + .returns({ + 'url': 'https://foo.com/hb/' + }); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() + .withGppConsent('foo', '123') .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -1747,11 +1857,11 @@ describe('33acrossBidAdapter:', function () { context('when SRA mode is enabled', function() { it('builds a single request with multiple imps corresponding to each group {siteId, productId}', function() { - sandbox.stub(config, 'getConfig').callsFake(() => { - return { + sandbox.stub(config, 'getConfig') + .withArgs('ttxSettings') + .returns({ enableSRAMode: true - } - }); + }); const bidRequests = new BidRequestsBuilder() .addBid() @@ -2219,11 +2329,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined` + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined` + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` } ] @@ -2239,11 +2349,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gdpr=1` + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=&gdpr=1` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gdpr=1` + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=&gdpr=1` } ]; @@ -2259,11 +2369,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined&gdpr=1` + url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=&gdpr=1` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined&gdpr=1` + url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=&gdpr=1` } ]; @@ -2279,11 +2389,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gdpr=0` + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=&gdpr=0` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gdpr=0` + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=&gdpr=0` } ]; expect(syncResults).to.deep.equal(expectedSyncs); @@ -2298,11 +2408,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined` + url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined` + url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=` } ]; expect(syncResults).to.deep.equal(expectedSyncs); @@ -2317,11 +2427,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined&gdpr=0` + url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=&gdpr=0` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined&gdpr=0` + url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=&gdpr=0` } ]; expect(syncResults).to.deep.equal(expectedSyncs); @@ -2336,11 +2446,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined` + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined` + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` } ] @@ -2356,11 +2466,54 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=foo` + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=foo&gpp=&gpp_sid=` + }, + { + type: 'iframe', + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=foo&gpp=&gpp_sid=` + } + ]; + + expect(syncResults).to.deep.equal(expectedSyncs); + }); + }); + + context('when there is no GPP data', function() { + it('returns sync urls with empty GPP params', function() { + spec.buildRequests(bidRequests); + + const syncResults = spec.getUserSyncs(syncOptions, {}); + const expectedSyncs = [ + { + type: 'iframe', + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` + }, + { + type: 'iframe', + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` + } + ] + + expect(syncResults).to.deep.equal(expectedSyncs); + }) + }); + + context('when there is GPP data', function() { + it('returns sync urls with GPP consent string & GPP Section ID as params', function() { + spec.buildRequests(bidRequests); + + const syncResults = spec.getUserSyncs(syncOptions, {}, {}, undefined, { + gppString: 'foo', + applicableSections: ['123', '456'] + }); + const expectedSyncs = [ + { + type: 'iframe', + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=foo&gpp_sid=123%2C456` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=foo` + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=foo&gpp_sid=123%2C456` } ]; diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index 5070d2b8845..4f6d7c4a6c5 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -2,7 +2,7 @@ import { thirthyThreeAcrossIdSubmodule } from 'modules/33acrossIdSystem.js'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; -import { uspDataHandler } from 'src/adapterManager.js'; +import { uspDataHandler, coppaDataHandler, gppDataHandler } from 'src/adapterManager.js'; describe('33acrossIdSystem', () => { describe('name', () => { @@ -67,41 +67,35 @@ describe('33acrossIdSystem', () => { expect(request.url).to.contain('gdpr=1'); }); + }); - context('and the consent string is given', () => { - it('should call endpoint with the GDPR consent string', () => { - [ - { consentString: '', expected: '' }, - { consentString: undefined, expected: '' }, - { consentString: 'foo', expected: 'foo' } - ].forEach(({ consentString, expected }, index) => { - const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ - params: { - pid: '12345' - } - }, { - gdprApplies: true, - consentString - }); - - callback(completeCallback); - - expect(server.requests[index].url).to.contain(`gdpr_consent=${expected}`); - }); + context('when GDPR doesn\'t apply', () => { + it('should call endpoint with \'gdpr=0\'', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }, { + gdprApplies: false }); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('gdpr=0'); }); }); - context('when GDPR doesn\'t apply', () => { - it('should call endpoint with \'gdpr=0\' and no GDPR consent string parameter', () => { + context('when the GDPR consent string is given', () => { + it('should call endpoint with the GDPR consent string', () => { const completeCallback = () => {}; const { callback } = thirthyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } }, { - gdprApplies: false, consentString: 'foo' }); @@ -109,8 +103,7 @@ describe('33acrossIdSystem', () => { const [request] = server.requests; - expect(request.url).to.contain('gdpr=0'); - expect(request.url).not.to.contain('gdpr_consent'); + expect(request.url).to.contain('gdpr_consent=foo'); }); }); @@ -157,6 +150,108 @@ describe('33acrossIdSystem', () => { }); }); + context('when coppa is enabled', () => { + it('should call endpoint with an enabled coppa signal', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + sinon.stub(coppaDataHandler, 'getCoppa').returns(true); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('coppa=1'); + + coppaDataHandler.getCoppa.restore(); + }); + }); + + context('when coppa is not enabled', () => { + it('should call endpoint with coppa signal not enabled', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + sinon.stub(coppaDataHandler, 'getCoppa').returns(false); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('coppa=0'); + + coppaDataHandler.getCoppa.restore(); + }); + }); + + context('when a GPP consent string is given', () => { + beforeEach(() => { + sinon.stub(gppDataHandler, 'getConsentData'); + }); + + afterEach(() => { + gppDataHandler.getConsentData.restore(); + }); + + it('should call endpoint with the GPP consent string', () => { + [ + { gppString: '', expected: '' }, + { gppString: undefined, expected: '' }, + { gppString: 'foo', expected: 'foo' }, + ].forEach(({ gppString, expected }, index) => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + gppDataHandler.getConsentData.onCall(index).returns({ + gppString + }); + + callback(completeCallback); + + expect(server.requests[index].url).to.contain(`gpp=${expected}`); + }); + }); + + it('should call endpoint with the GPP applicable sections', () => { + const gppString = 'foo'; + + [ + { applicableSections: [], expected: '' }, + { applicableSections: undefined, expected: '' }, + { applicableSections: ['1'], expected: '1' }, + { applicableSections: ['1', '2'], expected: '1%2C2' }, + ].forEach(({ applicableSections, expected }, index) => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + gppDataHandler.getConsentData.onCall(index).returns({ + gppString: 'foo', + applicableSections + }); + + callback(completeCallback); + + expect(server.requests[index].url).to.contain(`gpp_sid=${expected}`); + }); + }); + }); + context('when the partner ID is not given', () => { it('should log an error', () => { const logErrorSpy = sinon.spy(utils, 'logError'); diff --git a/test/spec/modules/a1MediaBidAdapter_spec.js b/test/spec/modules/a1MediaBidAdapter_spec.js new file mode 100644 index 00000000000..e1db2b9ad8d --- /dev/null +++ b/test/spec/modules/a1MediaBidAdapter_spec.js @@ -0,0 +1,248 @@ +import { spec } from 'modules/a1MediaBidAdapter.js'; +import { config } from 'src/config.js'; +import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js'; +import 'modules/currency.js'; +import 'modules/priceFloors.js'; +import { replaceAuctionPrice } from '../../../src/utils'; + +const ortbBlockParams = { + battr: [ 13 ], + bcat: ['IAB1-1'] +}; +const getBidderRequest = (isMulti = false) => { + return { + bidderCode: 'a1media', + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + bidderRequestId: '104e8d2392bd6f', + bids: [ + { + bidder: 'a1media', + params: {}, + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + mediaTypes: { + banner: { + sizes: [ + [ 320, 100 ], + ] + }, + ...(isMulti && { + video: { + mimes: ['video/mp4'] + }, + native: { + title: { + required: true, + }} + }) + }, + ...(isMulti && { + nativeOrtbRequest: { + ver: '1.2', + assets: [ + { + id: 0, + required: 1, + title: { + len: 140 + } + } + ] + } + }), + adUnitCode: 'test-div', + transactionId: 'cab00498-028b-4061-8f9d-a8d66c8cb91d', + bidId: '2e9f38ea93bb9e', + bidderRequestId: '104e8d2392bd6f', + } + ], + } +}; +const getConvertedBidReq = () => { + return { + cur: [ + 'JPY' + ], + imp: [ + { + banner: { + format: [ + { + h: 100, + w: 320 + }, + ], + topframe: 0 + }, + bidfloor: 0, + bidfloorcur: 'JPY', + id: '2e9f38ea93bb9e' + } + ], + test: 0, + } +}; + +const getBidderResponse = () => { + return { + body: { + id: 'bid-response', + cur: 'JPY', + seatbid: [ + { + bid: [{ + impid: '2e9f38ea93bb9e', + crid: 'creative-id', + cur: 'JPY', + price: 9, + }] + } + ] + } + } +} +const bannerAdm = '
'; +const videoAdm = 'testvast1'; +const nativeAdm = '{"ver":"1.2","link":{"url":"test_url"},"assets":[{"id":1,"required":1,"title":{"text":"native_title"}}]}'; +const macroAdm = '
'; +const macroNurl = 'https://d11.contentsfeed.com/dsp/win/example.com/SITE/a1/${AUCTION_PRICE}'; +const interpretedNurl = `
`; + +describe('a1MediaBidAdapter', function() { + describe('isValidRequest', function() { + const bid = { + bidder: 'a1media', + }; + + it('should return true always', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('buildRequests', function() { + let bidderRequest, convertedRequest; + beforeEach(function() { + bidderRequest = getBidderRequest(); + convertedRequest = getConvertedBidReq(); + }); + + it('should return expected request object', function() { + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + + expect(bidRequest.method).equal('POST'); + expect(bidRequest.url).equal('https://d11.contentsfeed.com/dsp/breq/a1'); + expect(bidRequest.data).deep.equal(convertedRequest); + }); + it('should set ortb blocking using params', function() { + bidderRequest.bids[0].params = ortbBlockParams; + + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + convertedRequest.bcat = ortbBlockParams.bcat; + convertedRequest.imp[0].banner.battr = ortbBlockParams.battr; + + expect(bidRequest.data).deep.equal(convertedRequest); + }); + + it('should set bidfloor when getFloor is available', function() { + bidderRequest.bids[0].getFloor = () => ({ currency: 'USD', floor: 999 }); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + + expect(bidRequest.data.imp[0].bidfloor).equal(999); + expect(bidRequest.data.imp[0].bidfloorcur).equal('USD'); + }); + + it('should set cur when currency config is configured', function() { + config.setConfig({ + currency: { + adServerCurrency: 'USD', + } + }); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + + expect(bidRequest.data.cur[0]).equal('USD'); + }); + + it('should set bidfloor and currency using params when modules not available', function() { + bidderRequest.bids[0].params.currency = 'USD'; + bidderRequest.bids[0].params.bidfloor = 0.99; + + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + convertedRequest.imp[0].bidfloor = 0.99; + convertedRequest.imp[0].bidfloorcur = 'USD'; + convertedRequest.cur[0] = 'USD'; + + expect(bidRequest.data).deep.equal(convertedRequest); + }); + }); + + describe('interpretResponse', function() { + describe('when request mediaType is single', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBidderRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + it('should set cpm using price attribute', function() { + const bidResPrice = 9; + bidderResponse.body.seatbid[0].bid[0].price = bidResPrice; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].cpm).equal(bidResPrice); + }); + it('should set mediaType using request mediaTypes', function() { + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(BANNER); + }); + }); + + describe('when request mediaType is multi', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBidderRequest(true); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + it('should set mediaType to video', function() { + bidderResponse.body.seatbid[0].bid[0].adm = videoAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(VIDEO); + }); + it('should set mediaType to native', function() { + bidderResponse.body.seatbid[0].bid[0].adm = nativeAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(NATIVE); + }); + it('should set mediaType to banner when adm is neither native or video', function() { + bidderResponse.body.seatbid[0].bid[0].adm = bannerAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(BANNER); + }); + }); + + describe('resolve the AUCTION_PRICE macro', function() { + let bidRequest; + beforeEach(function() { + const bidderRequest = getBidderRequest(true); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + }); + it('should return empty array when bid response has not contents', function() { + const emptyResponse = { body: '' }; + const interpretedRes = spec.interpretResponse(emptyResponse, bidRequest); + expect(interpretedRes.length).equal(0); + }); + it('should replace macro keyword if is exist', function() { + const bidderResponse = getBidderResponse(); + bidderResponse.body.seatbid[0].bid[0].adm = macroAdm; + bidderResponse.body.seatbid[0].bid[0].nurl = macroNurl; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + + const expectedResPrice = 9; + const expectedAd = replaceAuctionPrice(macroAdm, expectedResPrice) + replaceAuctionPrice(interpretedNurl, expectedResPrice); + + expect(interpretedRes[0].ad).equal(expectedAd); + }); + }); + }); +}) diff --git a/test/spec/modules/a1MediaRtdProvider_spec.js b/test/spec/modules/a1MediaRtdProvider_spec.js new file mode 100644 index 00000000000..2630e83fcf5 --- /dev/null +++ b/test/spec/modules/a1MediaRtdProvider_spec.js @@ -0,0 +1,105 @@ +import { subModuleObj } from 'modules/a1MediaRtdProvider.js'; +import { loadExternalScript } from '../../../src/adloader.js'; +import { A1_AUD_KEY, A1_SEG_KEY, getStorageData, storage } from '../../../modules/a1MediaRtdProvider.js'; +import { expect } from 'chai'; + +const configWithParams = { + name: 'a1Media', + waitForIt: true, + params: { + tagId: 'lb4test.min.js', + }, +}; +const configWithoutParams = { + name: 'a1Media', + waitForIt: true, + params: { + }, +}; + +const reqBidsConfigObj = { + ortb2Fragments: { + global: {} + } +}; +const a1TestOrtbObj = { + user: { + data: [ + { + name: 'a1mediagroup.com', + ext: { + segtax: 900 + }, + segment: [{id: 'test'}] + } + ], + ext: { + eids: [ + { + source: 'a1mediagroup.com', + uids: [ + { + id: 'tester', + atype: 1 + } + ] + } + ] + } + } +}; + +describe('a1MediaRtdProvider', function() { + describe('init', function() { + describe('initialize with expected params', function() { + it('successfully initialize with load script', function() { + expect(subModuleObj.init(configWithParams)).to.be.true; + expect(window.linkback.l).to.be.true; + expect(loadExternalScript.called).to.be.true; + expect(loadExternalScript.args[0][0]).to.deep.equal('https://linkback.contentsfeed.com/src/lb4test.min.js'); + }) + + it('successfully initialize but script is already exist', function() { + const linkback = { l: true }; + + expect(subModuleObj.init(configWithParams)).to.be.true; + expect(loadExternalScript.called).to.be.false; + }) + }); + + describe('initialize without expected params', function() { + afterEach(function() { + storage.setCookie(A1_SEG_KEY, '', 0); + }) + + it('successfully initialize when publisher side segment is exist in cookie', function() { + storage.setCookie(A1_SEG_KEY, 'test'); + expect(subModuleObj.init(configWithoutParams)).to.be.true; + expect(getStorageData(A1_SEG_KEY)).to.not.equal(''); + }) + it('fails initalize publisher sied segment is not exist', function() { + expect(subModuleObj.init(configWithoutParams)).to.be.false; + expect(getStorageData(A1_SEG_KEY)).to.equal(''); + }) + }) + }); + + describe('alterBidRequests', function() { + const callback = sinon.stub(); + + before(function() { + storage.setCookie(A1_SEG_KEY, 'test'); + storage.setDataInLocalStorage(A1_AUD_KEY, 'tester'); + }) + after(function() { + storage.setCookie(A1_SEG_KEY, '', 0); + storage.removeDataFromLocalStorage(A1_AUD_KEY); + }) + + it('alterBidRequests', function() { + subModuleObj.getBidRequestData(reqBidsConfigObj, callback); + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.include(a1TestOrtbObj); + expect(callback.calledOnce).to.be.true; + }) + }); +}) diff --git a/test/spec/modules/acuityadsBidAdapter_spec.js b/test/spec/modules/acuityadsBidAdapter_spec.js new file mode 100644 index 00000000000..31ef9dd6466 --- /dev/null +++ b/test/spec/modules/acuityadsBidAdapter_spec.js @@ -0,0 +1,428 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/acuityadsBidAdapter'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'acuityads' + +describe('AcuityAdsBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500, + ortb2: {} + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://prebid.admanmedia.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + describe('Returns data with gppConsent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/adagioAnalyticsAdapter_spec.js b/test/spec/modules/adagioAnalyticsAdapter_spec.js index 581f3cb1b87..8359a34baa0 100644 --- a/test/spec/modules/adagioAnalyticsAdapter_spec.js +++ b/test/spec/modules/adagioAnalyticsAdapter_spec.js @@ -1,12 +1,14 @@ import adagioAnalyticsAdapter from 'modules/adagioAnalyticsAdapter.js'; import { expect } from 'chai'; import * as utils from 'src/utils.js'; +import { getGlobal } from 'src/prebidGlobal.js'; +import { server } from 'test/mocks/xhr.js'; let adapterManager = require('src/adapterManager').default; let events = require('src/events'); let constants = require('src/constants.json'); -describe('adagio analytics adapter', () => { +describe('adagio analytics adapter - adagio.js', () => { let sandbox; let adagioQueuePushSpy; @@ -174,3 +176,601 @@ describe('adagio analytics adapter', () => { }); }); }); + +const AUCTION_ID = '25c6d7f5-699a-4bfc-87c9-996f915341fa'; +const AUCTION_ID_ADAGIO = '6fc53663-bde5-427b-ab63-baa9ed296f47' +const AUCTION_ID_CACHE = 'b43d24a0-13d4-406d-8176-3181402bafc4'; +const AUCTION_ID_CACHE_ADAGIO = 'a9cae98f-efb5-477e-9259-27350044f8db'; + +const BID_ADAGIO = Object.assign({}, BID_ADAGIO, { + bidder: 'adagio', + auctionId: AUCTION_ID, + adUnitCode: '/19968336/header-bid-tag-1', + bidId: '3bd4ebb1c900e2', + partnerImpId: 'partnerImpressionID-2', + adId: 'fake_ad_id_2', + requestId: '3bd4ebb1c900e2', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 1.42, + currency: 'USD', + originalCpm: 1.42, + originalCurrency: 'USD', + dealId: 'the-deal-id', + dealChannel: 'PMP', + mi: 'matched-impression', + seatBidId: 'aaaa-bbbb-cccc-dddd', + adserverTargeting: { + 'hb_bidder': 'another', + 'hb_adid': '3bd4ebb1c900e2', + 'hb_pb': '1.500', + 'hb_size': '728x90', + 'hb_source': 'server' + }, + meta: { + advertiserDomains: ['example.com'] + }, + seatId: '42', +}); + +const BID_ANOTHER = Object.assign({}, BID_ANOTHER, { + bidder: 'another', + auctionId: AUCTION_ID, + adUnitCode: '/19968336/header-bid-tag-1', + bidId: '3bd4ebb1c900e2', + partnerImpId: 'partnerImpressionID-2', + adId: 'fake_ad_id_2', + requestId: '3bd4ebb1c900e2', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 1.71, + currency: 'EUR', + originalCpm: 1.62, + originalCurrency: 'GBP', + dealId: 'the-deal-id', + dealChannel: 'PMP', + mi: 'matched-impression', + seatBidId: 'aaaa-bbbb-cccc-dddd', + adserverTargeting: { + 'hb_bidder': 'another', + 'hb_adid': '3bd4ebb1c900e2', + 'hb_pb': '1.500', + 'hb_size': '728x90', + 'hb_source': 'server' + }, + meta: { + advertiserDomains: ['example.com'] + } +}); + +const BID_CACHED = Object.assign({}, BID_ADAGIO, { + auctionId: AUCTION_ID_CACHE, + latestTargetedAuctionId: BID_ADAGIO.auctionId, +}); + +const PARAMS_ADG = { + organizationId: '1001', + site: 'test-com', + pageviewId: 'a68e6d70-213b-496c-be0a-c468ff387106', + environment: 'desktop', + pagetype: 'article', + placement: 'pave_top' +}; + +const AUCTION_INIT_ANOTHER = { + 'auctionId': AUCTION_ID, + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 100 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + }, { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG + }, + }, ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + }, { + 'code': '/19968336/footer-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } ], + 'adUnitCodes': ['/19968336/header-bid-tag-1', '/19968336/footer-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'another', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + }, { + 'bidderCode': 'adagio', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG, + adagioAuctionId: AUCTION_ID_ADAGIO + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 +}; + +const AUCTION_INIT_CACHE = { + 'auctionId': AUCTION_ID_CACHE, + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 100 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + }, { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG + }, + }, ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + }, { + 'code': '/19968336/footer-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } ], + 'adUnitCodes': ['/19968336/header-bid-tag-1', '/19968336/footer-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'another', + 'auctionId': AUCTION_ID_CACHE, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + }, { + 'bidderCode': 'adagio', + 'auctionId': AUCTION_ID_CACHE, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG, + adagioAuctionId: AUCTION_ID_CACHE_ADAGIO + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 +}; + +const MOCK = { + SET_TARGETING: { + [BID_ADAGIO.adUnitCode]: BID_ADAGIO.adserverTargeting, + [BID_ANOTHER.adUnitCode]: BID_ANOTHER.adserverTargeting + }, + AUCTION_INIT: { + another: AUCTION_INIT_ANOTHER, + bidcached: AUCTION_INIT_CACHE + }, + BID_RESPONSE: { + adagio: BID_ADAGIO, + another: BID_ANOTHER + }, + BID_WON: { + adagio: Object.assign({}, BID_ADAGIO, { + 'status': 'rendered' + }), + another: Object.assign({}, BID_ANOTHER, { + 'status': 'rendered' + }), + bidcached: Object.assign({}, BID_CACHED, { + 'status': 'rendered' + }), + }, + AD_RENDER_SUCCEEDED: { + another: { + ad: '
ad
', + adId: 'fake_ad_id_2', + bid: BID_ANOTHER + }, + bidcached: { + ad: '
ad
', + adId: 'fake_ad_id_2', + bid: BID_CACHED + } + }, +}; + +describe('adagio analytics adapter', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + sandbox.stub(events, 'getEvents').returns([]); + + adapterManager.registerAnalyticsAdapter({ + code: 'adagio', + adapter: adagioAnalyticsAdapter + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('track', () => { + beforeEach(() => { + adapterManager.enableAnalytics({ + provider: 'adagio' + }); + }); + + afterEach(() => { + adagioAnalyticsAdapter.disableAnalytics(); + }); + + it('builds and sends auction data', () => { + getGlobal().convertCurrency = (cpm, from, to) => { + const convKeys = { + 'GBP-EUR': 0.7, + 'EUR-GBP': 1.3, + 'USD-EUR': 0.8, + 'EUR-USD': 1.2, + 'USD-GBP': 0.6, + 'GBP-USD': 1.6, + }; + return cpm * (convKeys[`${from}-${to}`] || 1); + }; + + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + events.emit(constants.EVENTS.BID_WON, MOCK.BID_WON.another); + events.emit(constants.EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED.another); + + expect(server.requests.length).to.equal(3); + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[0].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.dvc).to.equal('desktop'); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another'); + expect(search.adg_mts).to.equal('ban'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[1].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.adg_sid).to.equal('42'); + expect(search.win_bdr).to.equal('another'); + expect(search.win_mt).to.equal('ban'); + expect(search.win_ban_sz).to.equal('728x90'); + expect(search.win_cpm).to.equal('1.71'); + expect(search.cur).to.equal('EUR'); + expect(search.cur_rate).to.equal('1.2'); + expect(search.og_cpm).to.equal('1.62'); + expect(search.og_cur).to.equal('GBP'); + expect(search.og_cur_rate).to.equal('1.6'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('3'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.rndr).to.equal('1'); + } + }); + + it('builds and sends auction data with a cached bid win', () => { + getGlobal().convertCurrency = (cpm, from, to) => { + const convKeys = { + 'GBP-EUR': 0.7, + 'EUR-GBP': 1.3, + 'USD-EUR': 0.8, + 'EUR-USD': 1.2, + 'USD-GBP': 0.6, + 'GBP-USD': 1.6, + }; + return cpm * (convKeys[`${from}-${to}`] || 1); + }; + + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.bidcached); + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + events.emit(constants.EVENTS.BID_WON, MOCK.BID_WON.bidcached); + events.emit(constants.EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED.bidcached); + + expect(server.requests.length).to.equal(4); + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[0].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.dvc).to.equal('desktop'); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another'); + expect(search.adg_mts).to.equal('ban'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[1].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.dvc).to.equal('desktop'); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another'); + expect(search.adg_mts).to.equal('ban'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.auct_id_c).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.adg_sid).to.equal('42'); + expect(search.win_bdr).to.equal('adagio'); + expect(search.win_mt).to.equal('ban'); + expect(search.win_ban_sz).to.equal('728x90'); + expect(search.win_cpm).to.equal('1.42'); + expect(search.cur).to.equal('USD'); + expect(search.cur_rate).to.equal('1'); + expect(search.og_cpm).to.equal('1.42'); + expect(search.og_cur).to.equal('USD'); + expect(search.og_cur_rate).to.equal('1'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[3].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('3'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.auct_id_c).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.rndr).to.equal('1'); + } + }); + }); +}); diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 2fb40070184..1f734a6a7fc 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -256,7 +256,6 @@ describe('Adagio bid adapter', () => { describe('buildRequests()', function() { const expectedDataKeys = [ - 'id', 'organizationId', 'secure', 'device', @@ -269,7 +268,8 @@ describe('Adagio bid adapter', () => { 'prebidVersion', 'featuresVersion', 'data', - 'usIfr' + 'usIfr', + 'adgjs', ]; it('groups requests by organizationId', function() { @@ -322,6 +322,21 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.adUnits[0].transactionId).to.not.exist; }); + it('should enrich prebid bid requests params', function() { + const expectedAuctionId = '373bcda7-9794-4f1c-be2c-0d223d11d579' + const expectedPageviewId = '56befc26-8cf0-472d-b105-73896df8eb89'; + sandbox.stub(utils, 'generateUUID').returns(expectedAuctionId); + sandbox.stub(adagio, 'getPageviewId').returns(expectedPageviewId); + + const bid01 = new BidRequestBuilder().withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + + spec.buildRequests([bid01], bidderRequest); + + expect(bid01.params.adagioAuctionId).eq(expectedAuctionId); + expect(bid01.params.pageviewId).eq(expectedPageviewId); + }); + it('should enqueue computed features for collect usage', function() { sandbox.stub(Date, 'now').returns(12345); @@ -697,6 +712,95 @@ describe('Adagio bid adapter', () => { }); }); + describe('with GPP', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + const regsGpp = 'regs_gpp_consent_string'; + const regsApplicableSections = [2]; + + const ortb2Gpp = 'ortb2_gpp_consent_string'; + const ortb2GppSid = [1]; + + context('When GPP in regs module', function() { + it('send gpp and gppSid to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + applicableSections: regsApplicableSections, + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(regsGpp); + expect(requests[0].data.regs.gppSid).to.equal(regsApplicableSections); + }); + }); + + context('When GPP partially defined in regs module', function() { + it('send gpp and gppSid coming from ortb2 to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + }, + ortb2: { + regs: { + gpp: ortb2Gpp, + gpp_sid: ortb2GppSid, + } + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(ortb2Gpp); + expect(requests[0].data.regs.gppSid).to.equal(ortb2GppSid); + }); + + it('send empty gpp and gppSid if no ortb2 fields to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(''); + expect(requests[0].data.regs.gppSid).to.be.empty; + }); + }); + + context('When GPP defined in ortb2 module', function() { + it('send gpp and gppSid coming from ortb2 to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + ortb2: { + regs: { + gpp: ortb2Gpp, + gpp_sid: ortb2GppSid, + } + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(ortb2Gpp); + expect(requests[0].data.regs.gppSid).to.equal(ortb2GppSid); + }); + }); + + context('When GPP not defined in any modules', function() { + it('send empty gpp and gppSid', function() { + const bidderRequest = new BidderRequestBuilder({}).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(''); + expect(requests[0].data.regs.gppSid).to.be.empty; + }); + }); + }); + describe('with userID modules', function() { const userIdAsEids = [{ 'source': 'pubcid.org', diff --git a/test/spec/modules/adfBidAdapter_spec.js b/test/spec/modules/adfBidAdapter_spec.js index 88f595b6c00..c1acff522c0 100644 --- a/test/spec/modules/adfBidAdapter_spec.js +++ b/test/spec/modules/adfBidAdapter_spec.js @@ -1,4 +1,5 @@ // jshint esversion: 6, es3: false, node: true +/* eslint-disable no-console */ import { assert } from 'chai'; import { spec } from 'modules/adfBidAdapter.js'; import { config } from 'src/config.js'; @@ -446,6 +447,52 @@ describe('Adf adapter', function () { }); }); + it('should add correct params to getFloor', function () { + let result; + let mediaTypes = { video: { + playerSize: [ 100, 200 ] + } }; + const expectedFloors = [ 1, 1.3, 0.5 ]; + config.setConfig({ currency: { adServerCurrency: 'DKK' } }); + let validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + mediaTypes = { banner: { + sizes: [ [100, 200], [300, 400] ] + }}; + validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + mediaTypes = { native: {} }; + validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + mediaTypes = {}; + validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + function getBidWithFloorTest(floor) { + return { + params: { mid: 1 }, + mediaTypes: mediaTypes, + getFloor: (args) => { + result = args; + return { + currency: 'DKK', + floor + }; + } + }; + } + }); + function getBidWithFloor(floor) { return { params: { mid: 1 }, diff --git a/test/spec/modules/adfusionBidAdapter_spec.js b/test/spec/modules/adfusionBidAdapter_spec.js new file mode 100644 index 00000000000..638831c33f3 --- /dev/null +++ b/test/spec/modules/adfusionBidAdapter_spec.js @@ -0,0 +1,97 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adfusionBidAdapter'; +import 'modules/priceFloors.js'; +import { newBidder } from 'src/adapters/bidderFactory'; + +describe('adfusionBidAdapter', function () { + const adapter = newBidder(spec); + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when params.accountID is missing', function () { + let localbid = Object.assign({}, bid); + delete localbid.params.accountId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests, bidderRequest; + beforeEach(function () { + bidRequests = [ + { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + }, + { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480], + }, + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2', + }, + ]; + bidderRequest = { refererInfo: {} }; + }); + + it('should return an empty array when no bid requests', function () { + const bidRequest = spec.buildRequests([], bidderRequest); + expect(bidRequest).to.be.an('array'); + expect(bidRequest.length).to.equal(0); + }); + + it('should return a valid bid request object', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request).to.be.an('array'); + expect(request[0].data).to.be.an('object'); + expect(request[0].method).to.equal('POST'); + expect(request[0].url).to.not.equal(''); + expect(request[0].url).to.not.equal(undefined); + expect(request[0].url).to.not.equal(null); + }); + }); +}); diff --git a/test/spec/modules/adgenerationBidAdapter_spec.js b/test/spec/modules/adgenerationBidAdapter_spec.js index de8463731e0..adfd38d22cc 100644 --- a/test/spec/modules/adgenerationBidAdapter_spec.js +++ b/test/spec/modules/adgenerationBidAdapter_spec.js @@ -184,12 +184,12 @@ describe('AdgenerationAdapter', function () { } }; const data = { - banner: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerUSD: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=USD&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&imark=1&tp=https%3A%2F%2Fexample.com`, - native: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=1x1¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&tp=https%3A%2F%2Fexample.com`, - bannerWithHyperId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&imark=1&tp=https%3A%2F%2Fexample.com&hyper_id=novatiqId`, - bannerWithAdgextCriteoId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&adgext_criteo_id=criteo-id-test-1234567890&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerWithAdgextIds: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&adgext_id5_id=id5-id-test-1234567890&adgext_id5_id_link_type=2&adgext_imuid=i.KrAH6ZAZTJOnH5S4N2sogA&adgext_uid2=%5Bobject%20Object%5D&gpid=%252F1111%252Fhomepage%2523300x250&uach=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22macOS%22%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Not%3AA-Brand%22%2C%22version%22%3A%5B%2299%22%5D%7D%5D%2C%22mobile%22%3A0%7D&schain=%257B%2522ver%2522%253A%25221.0%2522%252C%2522complete%2522%253A1%252C%2522nodes%2522%253A%255B%257B%2522asi%2522%253A%2522indirectseller.com%2522%252C%2522sid%2522%253A%252200001%2522%252C%2522hp%2522%253A1%257D%255D%257D&imark=1&tp=https%3A%2F%2Fexample.com`, + banner: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com`, + bannerUSD: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=USD&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com`, + native: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=1x1¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&tp=https%3A%2F%2Fexample.com`, + bannerWithHyperId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com&hyper_id=novatiqId`, + bannerWithAdgextCriteoId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&adgext_criteo_id=criteo-id-test-1234567890&imark=1&tp=https%3A%2F%2Fexample.com`, + bannerWithAdgextIds: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&adgext_id5_id=id5-id-test-1234567890&adgext_id5_id_link_type=2&adgext_imuid=i.KrAH6ZAZTJOnH5S4N2sogA&adgext_uid2=AgAAAAVacu1uAxgAxH%2BHJ8%2BnWlS2H4uVqr6i%2BHBDCNREHD8WKsio%2Fx7D8xXFuq1cJycUU86yXfTH9Xe%2F4C8KkH%2B7UCiU7uQxhyD7Qxnv251pEs6K8oK%2BBPLYR%2B8BLY%2FsJKesa%2FkoKwx1FHgUzIBum582tSy2Oo%2B7C6wYUaaV4QcLr%2F4LPA%3D&gpid=%2F1111%2Fhomepage%23300x250&uach=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22macOS%22%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Not%3AA-Brand%22%2C%22version%22%3A%5B%2299%22%5D%7D%5D%2C%22mobile%22%3A0%7D&schain=%7B%22ver%22%3A%221.0%22%2C%22complete%22%3A1%2C%22nodes%22%3A%5B%7B%22asi%22%3A%22indirectseller.com%22%2C%22sid%22%3A%2200001%22%2C%22hp%22%3A1%7D%5D%7D&imark=1&tp=https%3A%2F%2Fexample.com`, }; it('sends bid request to ENDPOINT via GET', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; diff --git a/test/spec/modules/adkernelAdnBidAdapter_spec.js b/test/spec/modules/adkernelAdnBidAdapter_spec.js index ff7ed9f145d..cfee5693cf5 100644 --- a/test/spec/modules/adkernelAdnBidAdapter_spec.js +++ b/test/spec/modules/adkernelAdnBidAdapter_spec.js @@ -426,8 +426,7 @@ describe('AdkernelAdn adapter', function () { describe('adapter configuration', () => { it('should have aliases', () => { - expect(spec.aliases).to.have.lengthOf(1); - expect(spec.aliases[0]).to.be.equal('engagesimply'); + expect(spec.aliases).to.be.an('array'); }); }); }); diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 45498d2734a..ac2e3785780 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -15,11 +15,13 @@ describe('Adkernel adapter', function () { auctionId: 'auc-001', mediaTypes: { banner: { - sizes: [[300, 250], [300, 200]] + sizes: [[300, 250], [300, 200]], + pos: 1 } }, ortb2Imp: { - battr: [6, 7, 9] + battr: [6, 7, 9], + pos: 2 } }, bid2_zone2 = { bidder: 'adkernel', @@ -103,7 +105,11 @@ describe('Adkernel adapter', function () { video: { context: 'instream', playerSize: [[640, 480]], - api: [1, 2] + api: [1, 2], + placement: 1, + plcmt: 1, + skip: 1, + pos: 1 } }, adUnitCode: 'ad-unit-1' @@ -346,6 +352,11 @@ describe('Adkernel adapter', function () { expect(bidRequest.imp[0].banner.battr).to.be.eql([6, 7, 9]); }); + it('should respect mediatypes attributes over FPD', function() { + expect(bidRequest.imp[0].banner).to.have.property('pos'); + expect(bidRequest.imp[0].banner.pos).to.be.eql(1); + }); + it('shouldn\'t contain gdpr nor ccpa information for default request', function () { let [_, bidRequests] = buildRequest([bid1_zone1]); expect(bidRequests[0]).to.not.have.property('regs'); @@ -354,11 +365,16 @@ describe('Adkernel adapter', function () { it('should contain gdpr-related information if consent is configured', function () { let [_, bidRequests] = buildRequest([bid1_zone1], - buildBidderRequest('https://example.com/index.html', - {gdprConsent: {gdprApplies: true, consentString: 'test-consent-string', vendorData: {}}, uspConsent: '1YNN'})); + buildBidderRequest('https://example.com/index.html', { + gdprConsent: {gdprApplies: true, consentString: 'test-consent-string', vendorData: {}}, + uspConsent: '1YNN', + gppConsent: {gppString: 'DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA', applicableSections: [2]}} + )); let bidRequest = bidRequests[0]; expect(bidRequest).to.have.property('regs'); expect(bidRequest.regs.ext).to.be.eql({'gdpr': 1, 'us_privacy': '1YNN'}); + expect(bidRequest.regs.gpp).to.be.eql('DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA'); + expect(bidRequest.regs.gpp_sid).to.be.eql([2]); expect(bidRequest).to.have.property('user'); expect(bidRequest.user.ext).to.be.eql({'consent': 'test-consent-string'}); }); @@ -433,8 +449,13 @@ describe('Adkernel adapter', function () { }); it('should have openrtb video impression parameters', function() { - expect(bidRequests[0].imp[0].video).to.have.property('api'); - expect(bidRequests[0].imp[0].video.api).to.be.eql([1, 2]); + let video = bidRequests[0].imp[0].video; + expect(video).to.have.property('api'); + expect(video.api).to.be.eql([1, 2]); + expect(video.placement).to.be.eql(1); + expect(video.plcmt).to.be.eql(1); + expect(video.skip).to.be.eql(1); + expect(video.pos).to.be.eql(1); }); }); diff --git a/test/spec/modules/adlooxAdServerVideo_spec.js b/test/spec/modules/adlooxAdServerVideo_spec.js index a071c6bbe3f..58277bc830d 100644 --- a/test/spec/modules/adlooxAdServerVideo_spec.js +++ b/test/spec/modules/adlooxAdServerVideo_spec.js @@ -1,11 +1,11 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; -import { ajax } from 'src/ajax.js'; import { buildVideoUrl } from 'modules/adlooxAdServerVideo.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; import { targeting } from 'src/targeting.js'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const analyticsAdapterName = 'adloox'; @@ -199,11 +199,9 @@ describe('Adloox Ad Server Video', function () { }); describe('process VAST', function () { - let server = null; let BID = null; let getWinningBidsStub; beforeEach(function () { - server = sinon.createFakeServer(); BID = utils.deepClone(bid); getWinningBidsStub = sinon.stub(targeting, 'getWinningBids') getWinningBidsStub.withArgs(adUnit.code).returns([ BID ]); @@ -212,8 +210,6 @@ describe('Adloox Ad Server Video', function () { getWinningBidsStub.restore(); getWinningBidsStub = undefined; BID = null; - server.restore(); - server = null; }); it('should return URL unchanged for non-VAST', function (done) { diff --git a/test/spec/modules/adlooxRtdProvider_spec.js b/test/spec/modules/adlooxRtdProvider_spec.js index 5b99789981f..0e26ef1afdb 100644 --- a/test/spec/modules/adlooxRtdProvider_spec.js +++ b/test/spec/modules/adlooxRtdProvider_spec.js @@ -1,12 +1,12 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; import {auctionManager} from 'src/auctionManager.js'; -import { config as _config } from 'src/config.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; import * as prebidGlobal from 'src/prebidGlobal.js'; import { subModuleObj as rtdProvider } from 'modules/adlooxRtdProvider.js'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const analyticsAdapterName = 'adloox'; @@ -139,16 +139,12 @@ describe('Adloox RTD Provider', function () { expect(analyticsAdapter.context).is.null; }); - let server = null; let CONFIG = null; beforeEach(function () { - server = sinon.createFakeServer(); CONFIG = utils.deepClone(config); }); afterEach(function () { CONFIG = null; - server.restore(); - server = null; }); it('should fetch segments', function (done) { diff --git a/test/spec/modules/admanBidAdapter_spec.js b/test/spec/modules/admanBidAdapter_spec.js index f1d9a7047d3..a9413860072 100644 --- a/test/spec/modules/admanBidAdapter_spec.js +++ b/test/spec/modules/admanBidAdapter_spec.js @@ -106,8 +106,11 @@ describe('AdmanAdapter', function () { let placements = data['placements']; for (let i = 0; i < placements.length; i++) { let placement = placements[i]; - expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'sizes', 'schain', 'bidFloor'); + expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'sizes', 'schain', 'bidFloor', 'ext'); expect(placement.schain).to.be.an('object') + expect(placement.ext).to.be.an('object') + expect(placement.ext).to.have.key('tid') + expect(placement.ext.tid).to.equal(bidBanner.transactionId); expect(placement.placementId).to.be.a('number'); expect(placement.bidId).to.be.a('string'); expect(placement.traffic).to.be.a('string'); @@ -132,7 +135,10 @@ describe('AdmanAdapter', function () { let placement = placements[i]; expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'schain', 'bidFloor', 'playerSize', 'minduration', 'maxduration', 'mimes', 'protocols', 'startdelay', 'placement', 'skip', - 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity'); + 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity', 'ext'); + expect(placement.ext).to.be.an('object') + expect(placement.ext).to.have.key('tid') + expect(placement.ext.tid).to.equal(bidBanner.transactionId); expect(placement.schain).to.be.an('object') expect(placement.placementId).to.be.a('number'); expect(placement.bidId).to.be.a('string'); diff --git a/test/spec/modules/admaticBidAdapter_spec.js b/test/spec/modules/admaticBidAdapter_spec.js index 1d2fb1e79cb..bd409958b1a 100644 --- a/test/spec/modules/admaticBidAdapter_spec.js +++ b/test/spec/modules/admaticBidAdapter_spec.js @@ -16,6 +16,10 @@ describe('admaticBidAdapter', () => { describe('isBidRequestValid', function() { let bid = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, 'bidder': 'admatic', 'params': { 'networkId': 10433394, @@ -28,6 +32,7 @@ describe('admaticBidAdapter', () => { 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', 'creativeId': 'er2ee', + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] } }; @@ -47,11 +52,16 @@ describe('admaticBidAdapter', () => { describe('buildRequests', function () { it('sends bid request to ENDPOINT via POST', function () { let validRequest = [ { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, 'bidder': 'admatic', 'params': { 'networkId': 10433394, 'host': 'layer.serve.admatic.com.tr' }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, 'mediaTypes': { 'banner': { @@ -149,6 +159,51 @@ describe('admaticBidAdapter', () => { 'placement': 2 }, 'id': '45e86fc7ce7fc93' + }, + { + 'size': [ + { + 'w': 1, + 'h': 1 + } + ], + 'type': 'native', + 'mediatype': { + 'title': { + 'required': true, + 'len': 120 + }, + 'image': { + 'required': true + }, + 'icon': { + 'required': false, + 'sizes': [ + 640, + 480 + ] + }, + 'sponsoredBy': { + 'required': false + }, + 'body': { + 'required': false + }, + 'clickUrl': { + 'required': false + }, + 'displayUrl': { + 'required': false + } + }, + 'ext': { + 'instl': 0, + 'gpid': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a', + 'data': { + 'pbadslot': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a' + } + }, + 'id': '16e0c8982318f91' } ], 'ext': { @@ -157,11 +212,16 @@ describe('admaticBidAdapter', () => { } } ]; let bidderRequest = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, 'bidder': 'admatic', 'params': { 'networkId': 10433394, 'host': 'layer.serve.admatic.com.tr' }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, 'mediaTypes': { 'banner': { @@ -259,6 +319,51 @@ describe('admaticBidAdapter', () => { 'placement': 2 }, 'id': '45e86fc7ce7fc93' + }, + { + 'size': [ + { + 'w': 1, + 'h': 1 + } + ], + 'type': 'native', + 'mediatype': { + 'title': { + 'required': true, + 'len': 120 + }, + 'image': { + 'required': true + }, + 'icon': { + 'required': false, + 'sizes': [ + 640, + 480 + ] + }, + 'sponsoredBy': { + 'required': false + }, + 'body': { + 'required': false + }, + 'clickUrl': { + 'required': false + }, + 'displayUrl': { + 'required': false + } + }, + 'ext': { + 'instl': 0, + 'gpid': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a', + 'data': { + 'pbadslot': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a' + } + }, + 'id': '16e0c8982318f91' } ], 'ext': { @@ -284,6 +389,7 @@ describe('admaticBidAdapter', () => { 'sizes': [[300, 250], [728, 90]] } }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, getFloor: inputParams => { if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { @@ -303,11 +409,16 @@ describe('admaticBidAdapter', () => { }, ]; let bidderRequest = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, 'bidder': 'admatic', 'params': { 'networkId': 10433394, 'host': 'layer.serve.admatic.com.tr' }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, 'adUnitCode': 'adunit-code', 'sizes': [[300, 250], [728, 90]], @@ -343,6 +454,7 @@ describe('admaticBidAdapter', () => { 'price': 0.01, 'type': 'banner', 'bidder': 'admatic', + 'mime_type': 'iframe', 'adomain': ['admatic.com.tr'], 'party_tag': '
', 'iurl': 'https://www.admatic.com.tr' @@ -354,6 +466,7 @@ describe('admaticBidAdapter', () => { 'height': 250, 'price': 0.01, 'type': 'video', + 'mime_type': 'iframe', 'bidder': 'admatic', 'adomain': ['admatic.com.tr'], 'party_tag': '', @@ -366,10 +479,24 @@ describe('admaticBidAdapter', () => { 'height': 250, 'price': 0.01, 'type': 'video', + 'mime_type': 'iframe', 'bidder': 'admatic', 'adomain': ['admatic.com.tr'], 'party_tag': 'https://www.admatic.com.tr', 'iurl': 'https://www.admatic.com.tr' + }, + { + 'id': 4, + 'creative_id': '3742', + 'width': 1, + 'height': 1, + 'price': 0.01, + 'type': 'native', + 'mime_type': 'iframe', + 'bidder': 'admatic', + 'adomain': ['admatic.com.tr'], + 'party_tag': '{"native":{"ver":"1.1","assets":[{"id":1,"title":{"text":"title"}},{"id":4,"data":{"value":"body"}},{"id":5,"data":{"value":"sponsored"}},{"id":2,"img":{"url":"https://www.admatic.com.tr","w":1200,"h":628}},{"id":3,"img":{"url":"https://www.admatic.com.tr","w":640,"h":480}}],"link":{"url":"https://www.admatic.com.tr"},"imptrackers":["https://www.admatic.com.tr"]}}', + 'iurl': 'https://www.admatic.com.tr' } ], 'queryId': 'cdnbh24rlv0hhkpfpln0', @@ -388,6 +515,7 @@ describe('admaticBidAdapter', () => { ad: '
', creativeId: '374', meta: { + model: 'iframe', advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -405,6 +533,7 @@ describe('admaticBidAdapter', () => { vastXml: '', creativeId: '3741', meta: { + model: 'iframe', advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -422,6 +551,40 @@ describe('admaticBidAdapter', () => { vastXml: 'https://www.admatic.com.tr', creativeId: '3741', meta: { + model: 'iframe', + advertiserDomains: ['admatic.com.tr'] + }, + ttl: 60, + bidder: 'admatic' + }, + { + requestId: 4, + cpm: 0.01, + width: 1, + height: 1, + currency: 'TRY', + mediaType: 'native', + netRevenue: true, + native: { + 'clickUrl': 'https://www.admatic.com.tr', + 'impressionTrackers': ['https://www.admatic.com.tr'], + 'title': 'title', + 'body': 'body', + 'sponsoredBy': 'sponsored', + 'image': { + 'url': 'https://www.admatic.com.tr', + 'width': 1200, + 'height': 628 + }, + 'icon': { + 'url': 'https://www.admatic.com.tr', + 'width': 640, + 'height': 480 + } + }, + creativeId: '3742', + meta: { + model: 'iframe', advertiserDomains: ['admatic.com.tr'] }, ttl: 60, diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index b1657ed1596..6a77c9205ca 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -12,7 +12,7 @@ describe('adnuntiusBidAdapter', function() { const EURO_URL = 'https://europe.delivery.adnuntius.com/i?tzo='; const GVLID = 855; const usi = utils.generateUUID() - const meta = [{ key: 'usi', value: usi }] + const meta = [{key: 'usi', value: usi}] before(() => { getGlobal().bidderSettings = { @@ -20,7 +20,7 @@ describe('adnuntiusBidAdapter', function() { storageAllowed: true } }; - const storage = getStorageManager({ bidderCode: 'adnuntius' }) + const storage = getStorageManager({bidderCode: 'adnuntius'}) storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)) }); @@ -99,9 +99,75 @@ describe('adnuntiusBidAdapter', function() { const videoBidRequest = { bid: videoBidderRequest, - bidder: 'adnuntius' + bidder: 'adnuntius', + params: { + bidType: 'justsomestuff-error-handling' + } } + const deals = [ + { + 'destinationUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fc%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg%3Fct%3D673%26r%3Dhttp%253A%252F%252Fadnuntius.com', + 'assets': { + 'Image': { + 'cdnId': 'https://cdn.adnuntius.com/cdn/iBgqruUNbaUb2hmD3vws7WTi84jg_WB_-VOF_FeOZ7A.png', + 'width': '640', + 'height': '480' + } + }, + 'text': {}, + 'choices': {}, + 'clickUrl': 'https://ads.adnuntius.delivery/c/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'urls': { + 'destination': 'https://ads.adnuntius.delivery/c/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg?ct=673&r=http%3A%2F%2Fadnuntius.com' + }, + 'urlsEsc': { + 'destination': 'https%3A%2F%2Fads.adnuntius.delivery%2Fc%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg%3Fct%3D673%26r%3Dhttp%253A%252F%252Fadnuntius.com' + }, + 'destinationUrls': { + 'destination': 'https://adnuntius.com' + }, + 'cpm': { + 'amount': 9, + 'currency': 'USD' + }, + 'bid': { + 'amount': 0.009, + 'currency': 'USD' + }, + 'grossBid': { + 'amount': 0.009, + 'currency': 'USD' + }, + 'netBid': { + 'amount': 0.0081, + 'currency': 'USD' + }, + 'dealId': 'abc123xyz', + 'impressionTrackingUrls': [], + 'impressionTrackingUrlsEsc': [], + 'adId': 'adn-id-1064238860', + 'selectedColumn': '0', + 'selectedColumnPosition': '0', + 'renderedPixel': 'https://ads.adnuntius.delivery/b/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg.html', + 'renderedPixelEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fb%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg.html', + 'visibleUrl': 'https://ads.adnuntius.delivery/s?rt=yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'visibleUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fs%3Frt%3DyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'viewUrl': 'https://ads.adnuntius.delivery/v?rt=yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'viewUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fv%3Frt%3DyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'rt': 'yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'creativeWidth': '640', + 'creativeHeight': '480', + 'creativeId': 's90t0q03pm', + 'lineItemId': 'cr3hnkkxhnkw9ldy', + 'layoutId': 'buyers_network_image_layout_1', + 'layoutName': 'Image', + 'layoutExternalReference': '', + 'html': "\n\n\n \n \n \n\n\n
\n
\"\"/
\n
\n \n\n", + 'renderTemplate': '' + } + ]; + const serverResponse = { body: { 'adUnits': [ @@ -111,68 +177,7 @@ describe('adnuntiusBidAdapter', function() { 'html': "\n\n\n \n \n \n\n\n
\n
\"\"/
\n
\n \n\n", 'matchedAdCount': 1, 'responseId': 'adn-rsp--229633088', - 'deals': [ - { - 'destinationUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fc%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg%3Fct%3D673%26r%3Dhttp%253A%252F%252Fadnuntius.com', - 'assets': { - 'Image': { - 'cdnId': 'https://cdn.adnuntius.com/cdn/iBgqruUNbaUb2hmD3vws7WTi84jg_WB_-VOF_FeOZ7A.png', - 'width': '640', - 'height': '480' - } - }, - 'text': {}, - 'choices': {}, - 'clickUrl': 'https://ads.adnuntius.delivery/c/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'urls': { - 'destination': 'https://ads.adnuntius.delivery/c/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg?ct=673&r=http%3A%2F%2Fadnuntius.com' - }, - 'urlsEsc': { - 'destination': 'https%3A%2F%2Fads.adnuntius.delivery%2Fc%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg%3Fct%3D673%26r%3Dhttp%253A%252F%252Fadnuntius.com' - }, - 'destinationUrls': { - 'destination': 'https://adnuntius.com' - }, - 'cpm': { - 'amount': 9, - 'currency': 'USD' - }, - 'bid': { - 'amount': 0.009, - 'currency': 'USD' - }, - 'grossBid': { - 'amount': 0.009, - 'currency': 'USD' - }, - 'netBid': { - 'amount': 0.0081, - 'currency': 'USD' - }, - 'dealId': 'abc123xyz', - 'impressionTrackingUrls': [], - 'impressionTrackingUrlsEsc': [], - 'adId': 'adn-id-1064238860', - 'selectedColumn': '0', - 'selectedColumnPosition': '0', - 'renderedPixel': 'https://ads.adnuntius.delivery/b/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg.html', - 'renderedPixelEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fb%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg.html', - 'visibleUrl': 'https://ads.adnuntius.delivery/s?rt=yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'visibleUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fs%3Frt%3DyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'viewUrl': 'https://ads.adnuntius.delivery/v?rt=yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'viewUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fv%3Frt%3DyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'rt': 'yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'creativeWidth': '640', - 'creativeHeight': '480', - 'creativeId': 's90t0q03pm', - 'lineItemId': 'cr3hnkkxhnkw9ldy', - 'layoutId': 'buyers_network_image_layout_1', - 'layoutName': 'Image', - 'layoutExternalReference': '', - 'html': "\n\n\n \n \n \n\n\n
\n
\"\"/
\n
\n \n\n", - 'renderTemplate': '' - } - ], + 'deals': deals, 'ads': [ { 'destinationUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fc%2F5Mu-vFVsaf4dWWx8uyZoV7Mz0sPkF1_j9bSupMwX8dMAAAAQCtjQz9kbGWD4nuZy3q6HaCYxqqTYxj2sS0FkMriztxcORshj3zRbT0KsV7XnDXE0F-OsHX7Ok6_FNLuIDTOMJyx6ZZCB10wGqA3OaSe1Eq9D85h8gP1gGsobC0KsAISm_PYNkJ6ve6qZLnB79fX6XHLYSRqTBM8sBCRXQAetnVzeo7AHoQhkFeouS444YA_q4JCTlRI2peWFIuxFlOYyeX9Kzg%3Fct%3D673%26r%3Dhttp%253A%252F%252Fadnuntius.com', @@ -747,12 +752,20 @@ describe('adnuntiusBidAdapter', function() { describe('interpretResponse', function() { it('should return valid response when passed valid server response', function() { - const interpretedResponse = spec.interpretResponse(serverResponse, singleBidRequest); + config.setBidderConfig({ + bidders: ['adnuntius'], + config: { + bidType: 'netBid', + maxDeals: 1 + } + }); + + const interpretedResponse = config.runWithBidder('adnuntius', () => spec.interpretResponse(serverResponse, singleBidRequest)); expect(interpretedResponse).to.have.lengthOf(2); const deal = serverResponse.body.adUnits[0].deals[0]; expect(interpretedResponse[0].bidderCode).to.equal('adnuntius'); - expect(interpretedResponse[0].cpm).to.equal(deal.bid.amount * 1000); + expect(interpretedResponse[0].cpm).to.equal(deal.netBid.amount * 1000); expect(interpretedResponse[0].width).to.equal(Number(deal.creativeWidth)); expect(interpretedResponse[0].height).to.equal(Number(deal.creativeHeight)); expect(interpretedResponse[0].creativeId).to.equal(deal.creativeId); @@ -768,7 +781,7 @@ describe('adnuntiusBidAdapter', function() { const ad = serverResponse.body.adUnits[0].ads[0]; expect(interpretedResponse[1].bidderCode).to.equal('adnuntius'); - expect(interpretedResponse[1].cpm).to.equal(ad.bid.amount * 1000); + expect(interpretedResponse[1].cpm).to.equal(ad.netBid.amount * 1000); expect(interpretedResponse[1].width).to.equal(Number(ad.creativeWidth)); expect(interpretedResponse[1].height).to.equal(Number(ad.creativeHeight)); expect(interpretedResponse[1].creativeId).to.equal(ad.creativeId); @@ -782,6 +795,59 @@ describe('adnuntiusBidAdapter', function() { expect(interpretedResponse[1].dealId).to.equal('not-in-deal-array-here'); expect(interpretedResponse[1].dealCount).to.equal(0); }); + + it('should not process valid response when passed alt bidder that is an adndeal', function() { + const altBidder = { + bid: [ + { + bidder: 'adndeal1', + bidId: 'adn-0000000000000551', + } + ] + }; + serverResponse.body.adUnits[0].deals = []; + + const interpretedResponse = spec.interpretResponse(serverResponse, altBidder); + expect(interpretedResponse).to.have.lengthOf(0); + + serverResponse.body.adUnits[0].deals = deals; + }); + + it('should return valid response when passed alt bidder', function() { + const altBidder = { + bid: [ + { + bidder: 'adn-alt', + bidId: 'adn-0000000000000551', + params: { + bidType: 'netBid' + } + } + ] + }; + serverResponse.body.adUnits[0].deals = []; + + const interpretedResponse = spec.interpretResponse(serverResponse, altBidder); + expect(interpretedResponse).to.have.lengthOf(1); + + const ad = serverResponse.body.adUnits[0].ads[0]; + expect(interpretedResponse[0].bidderCode).to.equal('adn-alt'); + expect(interpretedResponse[0].cpm).to.equal(ad.netBid.amount * 1000); + expect(interpretedResponse[0].width).to.equal(Number(ad.creativeWidth)); + expect(interpretedResponse[0].height).to.equal(Number(ad.creativeHeight)); + expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); + expect(interpretedResponse[0].currency).to.equal(ad.bid.currency); + expect(interpretedResponse[0].netRevenue).to.equal(false); + expect(interpretedResponse[0].meta).to.have.property('advertiserDomains'); + expect(interpretedResponse[0].meta.advertiserDomains).to.have.lengthOf(1); + expect(interpretedResponse[0].meta.advertiserDomains[0]).to.equal('adnuntius.com'); + expect(interpretedResponse[0].ad).to.equal(serverResponse.body.adUnits[0].html); + expect(interpretedResponse[0].ttl).to.equal(360); + expect(interpretedResponse[0].dealId).to.equal('not-in-deal-array-here'); + expect(interpretedResponse[0].dealCount).to.equal(0); + + serverResponse.body.adUnits[0].deals = deals; + }); }); describe('interpretVideoResponse', function() { diff --git a/test/spec/modules/adpod_spec.js b/test/spec/modules/adpod_spec.js index a6164f919ef..14e530c1a9b 100644 --- a/test/spec/modules/adpod_spec.js +++ b/test/spec/modules/adpod_spec.js @@ -47,7 +47,6 @@ describe('adpod.js', function () { addBidToAuctionStub = sinon.stub(auction, 'addBidToAuction').callsFake(function (auctionInstance, bid) { auctionBids.push(bid); }); - doCallbacksIfTimedoutStub = sinon.stub(auction, 'doCallbacksIfTimedout'); clock = sinon.useFakeTimers(); config.setConfig({ cache: { @@ -61,7 +60,6 @@ describe('adpod.js', function () { logWarnStub.restore(); logInfoStub.restore(); addBidToAuctionStub.restore(); - doCallbacksIfTimedoutStub.restore(); clock.restore(); config.resetConfig(); auctionBids = []; @@ -633,7 +631,6 @@ describe('adpod.js', function () { callPrebidCacheHook(callbackFn, auctionInstance, bidResponse1, afterBidAddedSpy, videoMT); callPrebidCacheHook(callbackFn, auctionInstance, bidResponse2, afterBidAddedSpy, videoMT); - expect(doCallbacksIfTimedoutStub.calledTwice).to.equal(true); expect(logWarnStub.calledOnce).to.equal(true); expect(auctionBids.length).to.equal(0); }); diff --git a/test/spec/modules/adqueryBidAdapter_spec.js b/test/spec/modules/adqueryBidAdapter_spec.js index 75b012aac77..e9286329d57 100644 --- a/test/spec/modules/adqueryBidAdapter_spec.js +++ b/test/spec/modules/adqueryBidAdapter_spec.js @@ -80,7 +80,11 @@ describe('adqueryBidAdapter', function () { }) describe('buildRequests', function () { - let req = spec.buildRequests([ bidRequest ], { refererInfo: { } })[0] + let req; + beforeEach(() => { + req = spec.buildRequests([ bidRequest ], { refererInfo: { } })[0] + }) + let rdata it('should return request object', function () { diff --git a/test/spec/modules/adqueryIdSystem_spec.js b/test/spec/modules/adqueryIdSystem_spec.js index a6b4e9d1529..0a2cd60d89e 100644 --- a/test/spec/modules/adqueryIdSystem_spec.js +++ b/test/spec/modules/adqueryIdSystem_spec.js @@ -1,5 +1,6 @@ -import { adqueryIdSubmodule, storage } from 'modules/adqueryIdSystem.js'; -import { server } from 'test/mocks/xhr.js'; +import {adqueryIdSubmodule, storage} from 'modules/adqueryIdSystem.js'; +import {server} from 'test/mocks/xhr.js'; +import sinon from 'sinon'; const config = { storage: { @@ -18,10 +19,10 @@ describe('AdqueryIdSystem', function () { }); }); - describe('getId', function() { + describe('getId', function () { let getDataFromLocalStorageStub; - beforeEach(function() { + beforeEach(function () { getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); @@ -29,7 +30,7 @@ describe('AdqueryIdSystem', function () { getDataFromLocalStorageStub.restore(); }); - it('gets a adqueryId', function() { + it('gets a adqueryId', function () { const config = { params: {} }; @@ -37,36 +38,24 @@ describe('AdqueryIdSystem', function () { const callback = adqueryIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq(`https://bidder.adquery.io/prebid/qid`); - request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ qid: 'qid' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'qid'}); + expect(request.url).to.contains(`https://bidder.adquery.io/prebid/qid?qid=`); + request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({qid: '6dd9eab7dfeab7df6dd9ea'})); + expect(callbackSpy.lastCall.lastArg).to.deep.equal('6dd9eab7dfeab7df6dd9ea'); }); - it('gets a cached adqueryId', function() { - const config = { - params: {} - }; - getDataFromLocalStorageStub.withArgs('qid').returns('qid'); - - const callbackSpy = sinon.spy(); - const callback = adqueryIdSubmodule.getId(config).callback; - callback(callbackSpy); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'qid'}); - }); - - it('allows configurable id url', function() { + it('allows configurable id url', function () { const config = { params: { - url: 'https://bidder.adquery.io' + url: 'https://another_bidder.adquery.io/qid' } }; const callbackSpy = sinon.spy(); const callback = adqueryIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq('https://bidder.adquery.io'); - request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ qid: 'testqid' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'testqid'}); + expect(request.url).to.contains('https://another_bidder.adquery.io/qid'); + request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({qid: 'testqid'})); + expect(callbackSpy.lastCall.lastArg).to.deep.equal('testqid'); }); }); }); diff --git a/test/spec/modules/adstirBidAdapter_spec.js b/test/spec/modules/adstirBidAdapter_spec.js new file mode 100644 index 00000000000..290a6822f69 --- /dev/null +++ b/test/spec/modules/adstirBidAdapter_spec.js @@ -0,0 +1,412 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/adstirBidAdapter.js'; +import * as utils from 'src/utils.js'; +import { config } from 'src/config.js'; + +describe('AdstirAdapter', function () { + describe('isBidRequestValid', function () { + it('should return true if appId is non-empty string and adSpaceNo is integer', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false if appId is non-empty string, but adSpaceNo is not integer', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 'a', + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if appId is non-empty string, but adSpaceNo is null', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: null, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if appId is non-empty string, but adSpaceNo is undefined', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX' + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is empty string', function () { + const bid = { + params: { + appId: '', + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is not string', function () { + const bid = { + params: { + appId: 123, + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is null', function () { + const bid = { + params: { + appId: null, + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is undefined', function () { + const bid = { + params: { + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if params is empty', function () { + const bid = { + params: {} + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const validBidRequests = [ + { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'adstir', + bidId: 'bidid1111', + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 1, + }, + transactionId: 'transactionid-1111', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [336, 280], + ], + } + }, + sizes: [ + [300, 250], + [336, 280], + ], + }, + { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'adstir', + bidId: 'bidid2222', + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 2, + }, + transactionId: 'transactionid-2222', + mediaTypes: { + banner: { + sizes: [ + [320, 50], + [320, 100], + ], + } + }, + sizes: [ + [320, 50], + [320, 100], + ], + }, + ]; + + const bidderRequest = { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + refererInfo: { + page: 'https://ad-stir.com/contact', + topmostLocation: 'https://ad-stir.com/contact', + reachedTop: true, + ref: 'https://test.example/q=adstir', + isAmp: false, + numIframes: 0, + stack: [ + 'https://ad-stir.com/contact', + ], + }, + }; + + it('one entry in validBidRequests corresponds to one server request object.', function () { + const requests = spec.buildRequests(validBidRequests, bidderRequest); + expect(requests.length).to.equal(validBidRequests.length); + requests.forEach(function (r, i) { + expect(r.method).to.equal('POST'); + expect(r.url).to.equal('https://ad.ad-stir.com/prebid'); + const d = JSON.parse(r.data); + expect(d.auctionId).to.equal('b06c5141-fe8f-4cdf-9d7d-54415490a917'); + + const v = validBidRequests[i]; + expect(d.appId).to.equal(v.params.appId); + expect(d.adSpaceNo).to.equal(v.params.adSpaceNo); + expect(d.bidId).to.equal(v.bidId); + expect(d.transactionId).to.equal(v.transactionId); + expect(d.mediaTypes).to.deep.equal(v.mediaTypes); + expect(d.sizes).to.deep.equal(v.sizes); + expect(d.ref.page).to.equal(bidderRequest.refererInfo.page); + expect(d.ref.tloc).to.equal(bidderRequest.refererInfo.topmostLocation); + expect(d.ref.referrer).to.equal(bidderRequest.refererInfo.ref); + expect(d.sua).to.equal(null); + expect(d.gdpr).to.equal(false); + expect(d.usp).to.equal(false); + expect(d.schain).to.equal(null); + expect(d.eids).to.deep.equal([]); + }); + }); + + it('ref.page, ref.tloc and ref.referrer correspond to refererInfo', function () { + const [ request ] = spec.buildRequests([validBidRequests[0]], { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + refererInfo: { + page: null, + topmostLocation: 'https://adserver.example/iframe1.html', + reachedTop: false, + ref: null, + isAmp: false, + numIframes: 2, + stack: [ + null, + 'https://adserver.example/iframe1.html', + 'https://adserver.example/iframe2.html' + ], + }, + }); + + const { ref } = JSON.parse(request.data); + expect(ref.page).to.equal(null); + expect(ref.tloc).to.equal('https://adserver.example/iframe1.html'); + expect(ref.referrer).to.equal(null); + }); + + it('when config.pageUrl is not set, ref.topurl equals to refererInfo.reachedTop', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (reachedTop) { + bidderRequestClone.refererInfo.reachedTop = reachedTop; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.ref.topurl).to.equal(reachedTop); + }); + }); + + describe('when config.pageUrl is set, ref.topurl is always false', function () { + before(function () { + config.setConfig({ pageUrl: 'https://ad-stir.com/register' }); + }); + after(function () { + config.resetConfig(); + }); + + it('ref.topurl should be false', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (reachedTop) { + bidderRequestClone.refererInfo.reachedTop = reachedTop; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.ref.topurl).to.equal(false); + }); + }); + }); + + it('gdprConsent.gdprApplies is sent', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (gdprApplies) { + bidderRequestClone.gdprConsent = { gdprApplies }; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.gdpr).to.equal(gdprApplies); + }); + }); + + it('includes in the request parameters whether CCPA applies', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + const cases = [ + { uspConsent: '1---', expected: false }, + { uspConsent: '1YYY', expected: true }, + { uspConsent: '1YNN', expected: true }, + { uspConsent: '1NYN', expected: true }, + { uspConsent: '1-Y-', expected: true }, + ]; + cases.forEach(function ({ uspConsent, expected }) { + bidderRequestClone.uspConsent = uspConsent; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.usp).to.equal(expected); + }); + }); + + it('should add schain if available', function() { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.example', + 'sid': '1234!abcd', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher, Inc.', + 'domain': 'publisher.example' + }, + { + 'asi': 'exchange2.example', + 'sid': 'abcd', + 'hp': 1, + 'rid': 'bid-request-2', + 'name': 'intermediary', + 'domain': 'intermediary.example' + } + ] + }; + const serializedSchain = '1.0,1!exchange1.example,1234%21abcd,1,bid-request-1,publisher%2C%20Inc.,publisher.example!exchange2.example,abcd,1,bid-request-2,intermediary,intermediary.example'; + + const [ request ] = spec.buildRequests([utils.mergeDeep(utils.deepClone(validBidRequests[0]), { schain })], bidderRequest); + const d = JSON.parse(request.data); + expect(d.schain).to.deep.equal(serializedSchain); + }); + + it('should add schain even if some nodes params are blank', function() { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.example', + 'sid': '1234!abcd', + 'hp': 1, + }, + { + }, + { + 'asi': 'exchange2.example', + 'sid': 'abcd', + 'hp': 1, + }, + ] + }; + const serializedSchain = '1.0,1!exchange1.example,1234%21abcd,1,,,!,,,,,!exchange2.example,abcd,1,,,'; + + const [ request ] = spec.buildRequests([utils.mergeDeep(utils.deepClone(validBidRequests[0]), { schain })], bidderRequest); + const d = JSON.parse(request.data); + expect(d.schain).to.deep.equal(serializedSchain); + }); + + it('should add UA client hints to payload if available', function () { + const sua = { + browsers: [ + { + brand: 'Not?A_Brand', + version: [ + '8', + '0', + '0', + '0' + ] + }, + { + version: [ + '108', + '0', + '5359', + '40' + ] + }, + { + brand: 'Google Chrome', + version: [ + '108', + '0', + '5359', + '40' + ] + } + ], + platform: { + brand: 'Android', + version: [ + '11' + ] + }, + mobile: 1, + architecture: '', + bitness: '64', + model: 'Pixel 5', + source: 2 + } + + const validBidRequestsClone = utils.deepClone(validBidRequests); + validBidRequestsClone[0] = utils.mergeDeep(validBidRequestsClone[0], { + ortb2: { + device: { sua }, + } + }); + + const requests = spec.buildRequests(validBidRequestsClone, bidderRequest); + requests.forEach(function (r) { + const d = JSON.parse(r.data); + expect(d.sua).to.deep.equal(sua); + }); + }); + }); + + describe('interpretResponse', function () { + it('return empty array when no content', function () { + const bids = spec.interpretResponse({ body: '' }); + expect(bids).to.deep.equal([]); + }); + it('return empty array when seatbid empty', function () { + const bids = spec.interpretResponse({ body: { seatbid: [] } }); + expect(bids).to.deep.equal([]); + }); + it('return valid bids when serverResponse is valid', function () { + const serverResponse = { + 'body': { + 'seatbid': [ + { + 'bid': { + 'ad': '
test response
', + 'cpm': 5250, + 'creativeId': '5_1234ABCD', + 'currency': 'JPY', + 'height': 250, + 'meta': { + 'advertiserDomains': [ + 'adv.example' + ], + 'mediaType': 'banner', + 'networkId': 5 + }, + 'netRevenue': true, + 'requestId': '22a9457aed98a4', + 'transactionId': 'f18c078e-4d2a-4ecb-a886-2a0c52187213', + 'ttl': 60, + 'width': 300, + } + } + ] + }, + 'headers': {} + }; + const bids = spec.interpretResponse(serverResponse); + expect(bids[0]).to.deep.equal(serverResponse.body.seatbid[0].bid); + }); + }); +}); diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index 26284d539d4..0acbaa06f5b 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -11,18 +11,13 @@ const EXPECTED_ENDPOINTS = [ 'https://ghb.adtelligent.com/v2/auction/' ]; const aliasEP = { - 'appaloosa': 'https://ghb.hb.appaloosa.media/v2/auction/', - 'appaloosa_publisherSuffix': 'https://ghb.hb.appaloosa.media/v2/auction/', - 'onefiftytwomedia': 'https://ghb.ads.152media.com/v2/auction/', - 'navelix': 'https://ghb.hb.navelix.com/v2/auction/', - 'bidsxchange': 'https://ghb.hbd.bidsxchange.com/v2/auction/', + 'janet_publisherSuffix': 'https://ghb.bidder.jmgads.com/v2/auction/', 'streamkey': 'https://ghb.hb.streamkey.net/v2/auction/', 'janet': 'https://ghb.bidder.jmgads.com/v2/auction/', - 'pgam': 'https://ghb.pgamssp.com/v2/auction/', 'ocm': 'https://ghb.cenarius.orangeclickmedia.com/v2/auction/', - 'vidcrunchllc': 'https://ghb.platform.vidcrunch.com/v2/auction/', '9dotsmedia': 'https://ghb.platform.audiodots.com/v2/auction/', 'copper6': 'https://ghb.app.copper6.com/v2/auction/', + 'indicue': 'https://ghb.console.indicue.com/v2/auction/', }; const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; @@ -156,6 +151,10 @@ const displayBidderRequestWithConsents = { gdprApplies: true, consentString: 'test' }, + gppConsent: { + gppString: 'abc12345234', + applicableSections: [7, 8] + }, uspConsent: 'iHaveIt' }; @@ -364,6 +363,10 @@ describe('adtelligentBidAdapter', () => { expect(bidRequestWithPubSettingsData.GDPR).to.be.equal(1); expect(bidRequestWithPubSettingsData.GDPRConsent).to.be.equal(displayBidderRequestWithConsents.gdprConsent.consentString); }); + it('sets GPP flags', () => { + expect(bidRequestWithPubSettingsData.GPP).to.be.equal(displayBidderRequestWithConsents.gppConsent.gppString); + expect(bidRequestWithPubSettingsData.GPPSid).to.be.equal('7,8'); + }); it('sets USP', () => { expect(bidRequestWithPubSettingsData.USP).to.be.equal(displayBidderRequestWithConsents.uspConsent); }) diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index 65c7584b428..e07e3a6e5d4 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -1,835 +1,19 @@ // jshint esversion: 6, es3: false, node: true -import {assert} from 'chai'; -import {spec} from 'modules/adxcgBidAdapter.js'; -import {config} from 'src/config.js'; -import {createEidsArray} from 'modules/userId/eids.js'; +import { assert } from 'chai'; +import { spec } from 'modules/adxcgBidAdapter.js'; +import { config } from 'src/config.js'; +import { createEidsArray } from 'modules/userId/eids.js'; +/* eslint dot-notation:0, quote-props:0 */ +import { expect } from 'chai'; + +import { syncAddFPDToBidderRequest } from '../../helpers/fpd.js'; +import { deepClone } from '../../../src/utils'; + const utils = require('src/utils'); describe('Adxcg adapter', function () { let bids = []; - describe('isBidRequestValid', function () { - let bid = { - 'bidder': 'adxcg', - 'params': { - 'adzoneid': '19910113' - } - }; - - it('should return true when required params found', function () { - assert(spec.isBidRequestValid(bid)); - - bid.params = { - adzoneid: 4332, - }; - assert(spec.isBidRequestValid(bid)); - }); - - it('should return false when required params are missing', function () { - bid.params = {}; - assert.isFalse(spec.isBidRequestValid(bid)); - - bid.params = { - mname: 'some-placement' - }; - assert.isFalse(spec.isBidRequestValid(bid)); - - bid.params = { - inv: 1234 - }; - assert.isFalse(spec.isBidRequestValid(bid)); - }); - }); - - describe('buildRequests', function () { - beforeEach(function () { - config.resetConfig(); - }); - it('should send request with correct structure', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: { - adzoneid: '19910113' - } - }]; - let request = spec.buildRequests(validBidRequests, {refererInfo: {page: 'page', domain: 'localhost'}}); - - assert.equal(request.method, 'POST'); - assert.equal(request.url, 'https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); - assert.deepEqual(request.options, {contentType: 'application/json'}); - assert.ok(request.data); - }); - - describe('user privacy', function () { - it('should send GDPR Consent data to exchange if gdprApplies', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = { - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user.ext.consent, bidderRequest.gdprConsent.consentString); - assert.equal(request.regs.ext.gdpr, bidderRequest.gdprConsent.gdprApplies); - assert.equal(typeof request.regs.ext.gdpr, 'number'); - }); - - it('should send gdpr as number', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = { - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(typeof request.regs.ext.gdpr, 'number'); - assert.equal(request.regs.ext.gdpr, 1); - }); - - it('should send CCPA Consent data to exchange', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = {uspConsent: '1YA-', refererInfo: {referer: 'page'}}; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.regs.ext.us_privacy, '1YA-'); - - bidderRequest = { - uspConsent: '1YA-', - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.regs.ext.us_privacy, '1YA-'); - assert.equal(request.user.ext.consent, 'consentDataString'); - assert.equal(request.regs.ext.gdpr, 1); - }); - - it('should not send GDPR Consent data to adxcg if gdprApplies is undefined', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let bidderRequest = { - gdprConsent: {gdprApplies: false, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user.ext.consent, 'consentDataString'); - assert.equal(request.regs.ext.gdpr, 0); - - bidderRequest = {gdprConsent: {consentString: 'consentDataString'}, refererInfo: {referer: 'page'}}; - request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user, undefined); - assert.equal(request.regs, undefined); - }); - it('should send default GDPR Consent data to exchange', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.equal(request.user, undefined); - assert.equal(request.regs, undefined); - }); - }); - - it('should add test and is_debug to request, if test is set in parameters', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {test: 1} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.ok(request.is_debug); - assert.equal(request.test, 1); - }); - - it('should have default request structure', function () { - let keys = 'site,geo,device,source,ext,imp'.split(','); - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - let data = Object.keys(request); - - assert.deepEqual(keys, data); - }); - - it('should set request keys correct values', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'}, - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, { - refererInfo: {referer: 'page'}, - ortb2: {source: {tid: 'tid'}} - }).data); - - assert.equal(request.source.tid, 'tid'); - assert.equal(request.source.fd, 1); - }); - - it('should send info about device', function () { - config.setConfig({ - device: {w: 100, h: 100} - }); - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {page: 'page', domain: 'localhost'}}).data); - - assert.equal(request.device.ua, navigator.userAgent); - assert.equal(request.device.w, 100); - assert.equal(request.device.h, 100); - }); - - it('should send app info', function () { - config.setConfig({ - app: {id: 'appid'}, - }); - const ortb2 = {app: {name: 'appname'}} - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - ortb2 - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}, ortb2}).data); - - assert.equal(request.app.id, 'appid'); - assert.equal(request.app.name, 'appname'); - assert.equal(request.site, undefined); - }); - - it('should send info about the site', function () { - config.setConfig({ - site: { - id: '123123', - publisher: { - domain: 'publisher.domain.com' - } - }, - }); - const ortb2 = { - site: { - publisher: { - id: 4441, - name: 'publisher\'s name' - } - } - }; - - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - ortb2 - }]; - let refererInfo = {page: 'page', domain: 'localhost'}; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo, ortb2}).data); - - assert.deepEqual(request.site, { - domain: 'localhost', - id: '123123', - page: refererInfo.page, - publisher: { - domain: 'publisher.domain.com', - id: 4441, - name: 'publisher\'s name' - } - }); - }); - - it('should pass extended ids', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {}, - userIdAsEids: createEidsArray({ - tdid: 'TTD_ID_FROM_USER_ID_MODULE', - pubcid: 'pubCommonId_FROM_USER_ID_MODULE' - }) - }]; - - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - assert.deepEqual(request.user.ext.eids, [ - {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, - {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} - ]); - }); - - it('should send currency if defined', function () { - config.setConfig({currency: {adServerCurrency: 'EUR'}}); - let validBidRequests = [{params: {}}]; - let refererInfo = {referer: 'page'}; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo}).data); - - assert.deepEqual(request.cur, ['EUR']); - }); - - it('should pass supply chain object', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {}, - schain: { - validation: 'strict', - config: { - ver: '1.0' - } - } - }]; - - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - assert.deepEqual(request.source.ext.schain, { - validation: 'strict', - config: { - ver: '1.0' - } - }); - }); - - describe('bids', function () { - it('should add more than one bid to the request', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }, { - bidId: 'bidId2', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.equal(request.imp.length, 2); - }); - it('should add incrementing values of id', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }, { - bidId: 'bidId2', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }, { - bidId: 'bidId3', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }]; - let imps = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - - for (let i = 0; i < 3; i++) { - assert.equal(imps[i].id, i + 1); - } - }); - - it('should add adzoneid', function () { - let validBidRequests = [{bidId: 'bidId', params: {adzoneid: 1000}, mediaTypes: {video: {}}}, - {bidId: 'bidId2', params: {adzoneid: 1001}, mediaTypes: {video: {}}}, - {bidId: 'bidId3', params: {adzoneid: 1002}, mediaTypes: {video: {}}}]; - let imps = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - for (let i = 0; i < 3; i++) { - assert.equal(imps[i].tagid, validBidRequests[i].params.adzoneid); - } - }); - - describe('price floors', function () { - it('should not add if floors module not configured', function () { - const validBidRequests = [{bidId: 'bidId', params: {adzoneid: 1000}, mediaTypes: {video: {}}}]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, undefined); - }); - - it('should not add if floor price not defined', function () { - const validBidRequests = [getBidWithFloor()]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, 'USD'); - }); - - it('should request floor price in adserver currency', function () { - config.setConfig({currency: {adServerCurrency: 'DKK'}}); - const validBidRequests = [getBidWithFloor()]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, 'DKK'); - }); - - it('should add correct floor values', function () { - const expectedFloors = [1, 1.3, 0.5]; - const validBidRequests = expectedFloors.map(getBidWithFloor); - let imps = getRequestImps(validBidRequests); - - expectedFloors.forEach((floor, index) => { - assert.equal(imps[index].bidfloor, floor); - assert.equal(imps[index].bidfloorcur, 'USD'); - }); - }); - - function getBidWithFloor(floor) { - return { - params: {adzoneid: 1}, - mediaTypes: {video: {}}, - getFloor: ({currency}) => { - return { - currency: currency, - floor - }; - } - }; - } - }); - - describe('multiple media types', function () { - it('should use all configured media types for bidding', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - }, - video: {} - } - }, { - bidId: 'bidId1', - params: {adzoneid: 1000}, - mediaTypes: { - video: {}, - native: {} - } - }, { - bidId: 'bidId2', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140} - }, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - }, - native: {}, - video: {} - } - }]; - let [first, second, third] = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - - assert.ok(first.banner); - assert.ok(first.video); - assert.equal(first.native, undefined); - - assert.ok(second.video); - assert.equal(second.banner, undefined); - assert.equal(second.native, undefined); - - assert.ok(third.native); - assert.ok(third.video); - assert.ok(third.banner); - }); - }); - - describe('banner', function () { - it('should convert sizes to openrtb format', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - } - } - }]; - let {banner} = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0]; - assert.deepEqual(banner, { - format: [{w: 100, h: 100}, {w: 200, h: 300}] - }); - }); - }); - - describe('video', function () { - it('should pass video mediatype config', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream', - mimes: ['video/mp4'] - } - } - }]; - let {video} = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0]; - assert.deepEqual(video, { - playerSize: [640, 480], - context: 'outstream', - mimes: ['video/mp4'] - }); - }); - }); - - describe('native', function () { - describe('assets', function () { - it('should set correct asset id', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - }]; - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - - assert.equal(assets[0].id, 0); - assert.equal(assets[1].id, 3); - assert.equal(assets[2].id, 4); - }); - it('should add required key if it is necessary', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140}, - sponsoredBy: {required: true, len: 140} - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - - assert.equal(assets[0].required, 1); - assert.ok(!assets[1].required); - assert.ok(!assets[2].required); - assert.equal(assets[3].required, 1); - }); - - it('should map img and data assets', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: true, sizes: [150, 50]}, - icon: {required: false, sizes: [50, 50]}, - body: {required: false, len: 140}, - sponsoredBy: {required: true}, - cta: {required: false}, - clickUrl: {required: false} - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].title); - assert.equal(assets[0].title.len, 140); - assert.deepEqual(assets[1].img, {type: 3, w: 150, h: 50}); - assert.deepEqual(assets[2].img, {type: 1, w: 50, h: 50}); - assert.deepEqual(assets[3].data, {type: 2, len: 140}); - assert.deepEqual(assets[4].data, {type: 1}); - assert.deepEqual(assets[5].data, {type: 12}); - assert.ok(!assets[6]); - }); - - describe('icon/image sizing', function () { - it('should flatten sizes and utilise first pair', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - sizes: [[200, 300], [100, 200]] - }, - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.w, 200); - assert.equal(assets[0].img.h, 300); - }); - }); - - it('should utilise aspect_ratios', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [{ - min_width: 100, - ratio_height: 3, - ratio_width: 1 - }] - }, - icon: { - aspect_ratios: [{ - min_width: 10, - ratio_height: 5, - ratio_width: 2 - }] - } - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.wmin, 100); - assert.equal(assets[0].img.hmin, 300); - - assert.ok(assets[1].img); - assert.equal(assets[1].img.wmin, 10); - assert.equal(assets[1].img.hmin, 25); - }); - - it('should not throw error if aspect_ratios config is not defined', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [] - }, - icon: { - aspect_ratios: [] - } - } - }]; - - assert.doesNotThrow(() => spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}})); - }); - }); - - it('should expect any dimensions if min_width not passed', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [{ - ratio_height: 3, - ratio_width: 1 - }] - } - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.wmin, 0); - assert.equal(assets[0].img.hmin, 0); - assert.ok(!assets[1]); - }); - }); - }); - - function getRequestImps(validBidRequests) { - return JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - } - }); - - describe('interpretResponse', function () { - it('should return if no body in response', function () { - let serverResponse = {}; - let bidRequest = {}; - - assert.ok(!spec.interpretResponse(serverResponse, bidRequest)); - }); - it('should return more than one bids', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{ - impid: '1', - native: {ver: '1.1', link: {url: 'link'}, assets: [{id: 1, title: {text: 'Asset title text'}}]} - }] - }, { - bid: [{ - impid: '2', - native: {ver: '1.1', link: {url: 'link'}, assets: [{id: 1, data: {value: 'Asset title text'}}]} - }] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - }, - { - bidId: 'bidId2', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(spec.interpretResponse(serverResponse, bidRequest).length, 2); - }); - - it('should set correct values to bid', function () { - let nativeExample1 = { - assets: [], - link: {url: 'link'}, - imptrackers: ['imptrackers url1', 'imptrackers url2'] - } - - let serverResponse = { - body: { - id: null, - bidid: null, - seatbid: [{ - bid: [ - { - impid: '1', - price: 93.1231, - crid: '12312312', - adm: JSON.stringify(nativeExample1), - dealid: 'deal-id', - adomain: ['demo.com'], - ext: { - crType: 'native', - advertiser_id: 'adv1', - advertiser_name: 'advname', - agency_name: 'agname', - mediaType: 'native' - } - } - ] - }], - cur: 'EUR' - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - } - ] - }; - - const bids = spec.interpretResponse(serverResponse, bidRequest); - const bid = serverResponse.body.seatbid[0].bid[0]; - assert.deepEqual(bids[0].requestId, bidRequest.bids[0].bidId); - assert.deepEqual(bids[0].cpm, bid.price); - assert.deepEqual(bids[0].creativeId, bid.crid); - assert.deepEqual(bids[0].ttl, 300); - assert.deepEqual(bids[0].netRevenue, false); - assert.deepEqual(bids[0].currency, serverResponse.body.cur); - assert.deepEqual(bids[0].mediaType, 'native'); - assert.deepEqual(bids[0].meta.mediaType, 'native'); - assert.deepEqual(bids[0].meta.advertiserDomains, ['demo.com']); - - assert.deepEqual(bids[0].meta.advertiserName, 'advname'); - assert.deepEqual(bids[0].meta.agencyName, 'agname'); - - assert.deepEqual(bids[0].dealId, 'deal-id'); - }); - - it('should return empty when there is no bids in response', function () { - const serverResponse = { - body: { - id: null, - bidid: null, - seatbid: [{bid: []}], - cur: 'EUR' - } - }; - let bidRequest = { - data: {}, - bids: [{bidId: 'bidId1'}] - }; - const result = spec.interpretResponse(serverResponse, bidRequest)[0]; - assert.ok(!result); - }); - - describe('banner', function () { - it('should set ad content on response', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{impid: '1', adm: '', ext: {crType: 'banner'}}] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000} - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(bids.length, 1); - assert.equal(bids[0].ad, ''); - assert.equal(bids[0].mediaType, 'banner'); - assert.equal(bids[0].meta.mediaType, 'banner'); - }); - }); - - describe('video', function () { - it('should set vastXml on response', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{impid: '1', adm: '', ext: {crType: 'video'}}] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000} - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(bids.length, 1); - assert.equal(bids[0].vastXml, ''); - assert.equal(bids[0].mediaType, 'video'); - assert.equal(bids[0].meta.mediaType, 'video'); - }); - }); - }); - describe('getUserSyncs', function () { const usersyncUrl = 'https://usersync-url.com'; beforeEach(() => { @@ -846,55 +30,55 @@ describe('Adxcg adapter', function () { }) it('should return user sync if pixel enabled with adxcg config', function () { - const ret = spec.getUserSyncs({pixelEnabled: true}) - expect(ret).to.deep.equal([{type: 'image', url: usersyncUrl}]) + const ret = spec.getUserSyncs({ pixelEnabled: true }) + expect(ret).to.deep.equal([{ type: 'image', url: usersyncUrl }]) }) it('should not return user sync if pixel disabled', function () { - const ret = spec.getUserSyncs({pixelEnabled: false}) + const ret = spec.getUserSyncs({ pixelEnabled: false }) expect(ret).to.be.an('array').that.is.empty }) it('should not return user sync if url is not set', function () { config.resetConfig() - const ret = spec.getUserSyncs({pixelEnabled: true}) + const ret = spec.getUserSyncs({ pixelEnabled: true }) expect(ret).to.be.an('array').that.is.empty }) - it('should pass GDPR consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ + it('should pass GDPR consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: false, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=0&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: undefined }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=` }]); }); - it('should pass US consent', function() { + it('should pass US consent', function () { expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?us_privacy=1NYN` }]); }); - it('should pass GDPR and US consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ + it('should pass GDPR and US consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }]); }); }); - describe('onBidWon', function() { - beforeEach(function() { + describe('onBidWon', function () { + beforeEach(function () { sinon.stub(utils, 'triggerPixel'); }); - afterEach(function() { + afterEach(function () { utils.triggerPixel.restore(); }); - it('Should trigger pixel if bid nurl', function() { + it('Should trigger pixel if bid nurl', function () { const bid = { nurl: 'http://example.com/win/${AUCTION_PRICE}', cpm: 2.1, @@ -904,4 +88,510 @@ describe('Adxcg adapter', function () { expect(utils.triggerPixel.callCount).to.equal(1) }) }) + + it('should return just to have at least 1 karma test ok', function () { + assert(true); + }); +}); + +describe('adxcg v8 oRtbConverter Adapter Tests', function () { + const slotConfigs = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[728, 90], [160, 600]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '300x250', + adzoneid: '77' + } + }, { + placementCode: '/DfpAccount2/slot2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bidId: 'bid23456', + params: { + cp: 'p10000', + ct: 't20000', + cf: '728x90', + adzoneid: '77' + } + }]; + const nativeOrtbRequest = { + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 150, + h: 50, + } + }, + { + id: 2, + required: 1, + title: { + len: 80 + } + }, + { + id: 3, + required: 0, + data: { + type: 1 + } + }] + }; + const nativeSlotConfig = [{ + placementCode: '/DfpAccount1/slot3', + bidId: 'bid12345', + mediaTypes: { + native: { + sendTargetingKeys: false, + ortb: nativeOrtbRequest + } + }, + nativeOrtbRequest, + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77' + } + }]; + const videoSlotConfig = [{ + placementCode: '/DfpAccount1/slotVideo', + bidId: 'bid12345', + mediaTypes: { + video: { + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77' + } + }]; + const additionalParamsConfig = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '1x1', + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345, + extra_key3: { + key1: 'val1', + key2: 23456, + }, + extra_key4: [1, 2, 3] + } + }]; + + const schainParamsSlotConfig = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '1x1', + adzoneid: '77', + bcat: ['IAB-1', 'IAB-20'], + battr: [1, 2, 3], + bidfloor: 1.5, + badv: ['cocacola.com', 'lays.com'] + }, + schain: { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + } + ] + }, + }]; + + const bidderRequest = { + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + } + }; + + it('Verify build request', function () { + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // site object + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site.publisher).to.not.equal(null); + // expect(ortbRequest.site.publisher.id).to.equal('p10000'); + expect(ortbRequest.site.page).to.equal('https://publisher.com/home'); + expect(ortbRequest.imp).to.have.lengthOf(2); + // device object + expect(ortbRequest.device).to.not.equal(null); + expect(ortbRequest.device.ua).to.equal(navigator.userAgent); + // slot 1 + // expect(ortbRequest.imp[0].tagid).to.equal('t10000'); + expect(ortbRequest.imp[0].banner).to.not.equal(null); + expect(ortbRequest.imp[0].banner.format).to.deep.eq([{ 'w': 728, 'h': 90 }, { 'w': 160, 'h': 600 }]); + // slot 2 + // expect(ortbRequest.imp[1].tagid).to.equal('t20000'); + expect(ortbRequest.imp[1].banner).to.not.equal(null); + expect(ortbRequest.imp[1].banner.format).to.deep.eq([{ 'w': 728, 'h': 90 }]); + }); + + it('Verify parse response', function () { + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + crid: 'Creative#123', + mtype: 1, + w: 300, + h: 250, + exp: 20, + adomain: ['advertiser.com'] + }] + }] + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.ad).to.equal('This is an Ad'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creative_id).to.equal('Creative#123'); + expect(bid.creativeId).to.equal('Creative#123'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('EUR'); + expect(bid.ttl).to.equal(20); + expect(bid.meta).to.not.be.null; + expect(bid.meta.advertiserDomains).to.eql(['advertiser.com']); + }); + + it('Verify full passback', function () { + const request = spec.buildRequests(slotConfigs, bidderRequest); + const bids = spec.interpretResponse({ body: null }, request) + expect(bids).to.have.lengthOf(0); + }); + + if (FEATURES.NATIVE) { + it('Verify Native request', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // native impression + expect(ortbRequest.imp[0].tagid).to.equal('77'); + expect(ortbRequest.imp[0].banner).to.be.undefined; + const nativePart = ortbRequest.imp[0]['native']; + expect(nativePart).to.not.equal(null); + expect(nativePart.request).to.not.equal(null); + // native request assets + const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); + expect(nativeRequest).to.not.equal(null); + expect(nativeRequest.assets).to.have.lengthOf(3); + // image asset + expect(nativeRequest.assets[0].id).to.equal(1); + expect(nativeRequest.assets[0].required).to.equal(1); + expect(nativeRequest.assets[0].title).to.be.undefined; + expect(nativeRequest.assets[0].img).to.not.equal(null); + expect(nativeRequest.assets[0].img.w).to.equal(150); + expect(nativeRequest.assets[0].img.h).to.equal(50); + expect(nativeRequest.assets[0].img.type).to.equal(3); + // title asset + expect(nativeRequest.assets[1].id).to.equal(2); + expect(nativeRequest.assets[1].required).to.equal(1); + expect(nativeRequest.assets[1].title).to.not.equal(null); + expect(nativeRequest.assets[1].title.len).to.equal(80); + // data asset + expect(nativeRequest.assets[2].id).to.equal(3); + expect(nativeRequest.assets[2].required).to.equal(0); + expect(nativeRequest.assets[2].title).to.be.undefined; + expect(nativeRequest.assets[2].data).to.not.equal(null); + expect(nativeRequest.assets[2].data.type).to.equal(1); + }); + + it('Verify Native response', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + const nativeResponse = { + assets: [ + { id: 1, img: { type: 3, url: 'https://images.cdn.brand.com/123' } }, + { id: 2, title: { text: 'Ad Title' } }, + { id: 3, data: { type: 1, value: 'Sponsored By: Brand' } } + ], + link: { url: 'https://brand.clickme.com/' }, + imptrackers: ['https://imp1.trackme.com/', 'https://imp1.contextweb.com/'] + + }; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: JSON.stringify(nativeResponse), + mtype: 4 + }] + }] + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + // verify bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.requestId).to.equal('bid12345'); + expect(bid.ad).to.be.undefined; + expect(bid.mediaType).to.equal('native'); + expect(bid['native']).to.not.be.null; + expect(bid['native'].ortb).to.not.be.null; + const nativeBid = bid['native'].ortb; + expect(nativeBid.assets).to.have.lengthOf(3); + expect(nativeBid.assets[0].id).to.equal(1); + expect(nativeBid.assets[0].img).to.not.be.null; + expect(nativeBid.assets[0].img.type).to.equal(3); + expect(nativeBid.assets[0].img.url).to.equal('https://images.cdn.brand.com/123'); + expect(nativeBid.assets[1].id).to.equal(2); + expect(nativeBid.assets[1].title).to.not.be.null; + expect(nativeBid.assets[1].title.text).to.equal('Ad Title'); + expect(nativeBid.assets[2].id).to.equal(3); + expect(nativeBid.assets[2].data).to.not.be.null; + expect(nativeBid.assets[2].data.type).to.equal(1); + expect(nativeBid.assets[2].data.value).to.equal('Sponsored By: Brand'); + expect(nativeBid.link).to.not.be.null; + expect(nativeBid.link.url).to.equal('https://brand.clickme.com/'); + expect(nativeBid.imptrackers).to.have.lengthOf(2); + expect(nativeBid.imptrackers[0]).to.equal('https://imp1.trackme.com/'); + expect(nativeBid.imptrackers[1]).to.equal('https://imp1.contextweb.com/'); + }); + } + + it('Verifies bidder code', function () { + expect(spec.code).to.equal('adxcg'); + }); + + it('Verifies bidder aliases', function () { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('mediaopti'); + }); + + it('Verifies supported media types', function () { + expect(spec.supportedMediaTypes).to.have.lengthOf(3); + expect(spec.supportedMediaTypes[0]).to.equal('banner'); + expect(spec.supportedMediaTypes[1]).to.equal('native'); + expect(spec.supportedMediaTypes[2]).to.equal('video'); + }); + + if (FEATURES.VIDEO) { + it('Verify Video request', function () { + const request = spec.buildRequests(videoSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].video).to.not.be.null; + expect(ortbRequest.imp[0].native).to.be.undefined; + expect(ortbRequest.imp[0].banner).to.be.undefined; + expect(ortbRequest.imp[0].video.w).to.equal(400); + expect(ortbRequest.imp[0].video.h).to.equal(300); + expect(ortbRequest.imp[0].video.minduration).to.equal(5); + expect(ortbRequest.imp[0].video.maxduration).to.equal(10); + expect(ortbRequest.imp[0].video.startdelay).to.equal(0); + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.minbitrate).to.equal(200); + expect(ortbRequest.imp[0].video.protocols).to.eql([1, 2, 4]); + }); + } + + it('Verify extra parameters', function () { + let request = spec.buildRequests(additionalParamsConfig, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].ext).to.not.equal(null); + expect(ortbRequest.imp[0].ext.prebid).to.not.equal(null); + expect(ortbRequest.imp[0].ext.prebid).to.not.be.null; + expect(ortbRequest.imp[0].ext.prebid.extra_key1).to.equal('extra_val1'); + expect(ortbRequest.imp[0].ext.prebid.extra_key2).to.equal(12345); + expect(ortbRequest.imp[0].ext.prebid.extra_key3).to.not.be.null; + expect(ortbRequest.imp[0].ext.prebid.extra_key3.key1).to.equal('val1'); + expect(ortbRequest.imp[0].ext.prebid.extra_key3.key2).to.equal(23456); + expect(ortbRequest.imp[0].ext.prebid.extra_key4).to.eql([1, 2, 3]); + expect(Object.keys(ortbRequest.imp[0].ext.prebid)).to.eql(['adzoneid', 'extra_key1', 'extra_key2', 'extra_key3', 'extra_key4']); + // attempting with a configuration with no unknown params. + request = spec.buildRequests(videoSlotConfig, bidderRequest); + ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + // expect(ortbRequest.imp[0].ext).to.be.undefined; + }); + + it('Verify user level first party data', function () { + const bidderRequest = { + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + }, + gdprConsent: { + gdprApplies: true, + consentString: 'serialized_gpdr_data' + }, + ortb2: { + user: { + yob: 1985, + gender: 'm', + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + } + } + }; + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.user).to.not.equal(null); + }); + + it('Verify site level first party data', function () { + const bidderRequest = { + ortb2: { + site: { + content: { + data: [{ + name: 'www.iris.com', + ext: { + segtax: 500, + cids: ['iris_c73g5jq96mwso4d8'] + } + }] + }, + page: 'http://pub.com/news', + ref: 'http://google.com', + publisher: { + domain: 'pub.com' + } + } + } + }; + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site).to.deep.equal({ + content: { + data: [{ + name: 'www.iris.com', + ext: { + segtax: 500, + cids: ['iris_c73g5jq96mwso4d8'] + } + }] + }, + page: 'http://pub.com/news', + ref: 'http://google.com', + publisher: { + // id: 'p10000', + domain: 'pub.com' + } + }); + }); + + it('Verify impression/slot level first party data', function () { + const bidderRequests = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345 + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } + } + } + }]; + let request = spec.buildRequests(bidderRequests, bidderRequest); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].ext).to.not.equal(null); + expect(ortbRequest.imp[0].ext).to.deep.equal({ + prebid: { + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345 + }, + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } + }); + }); + + it('Verify bid request timeouts', function () { + const mkRequest = (bidderRequest) => spec.buildRequests(slotConfigs, bidderRequest).data; + // assert default is used when no bidderRequest.timeout value is available + expect(mkRequest(bidderRequest).tmax).to.equal(500) + + // assert bidderRequest value is used when available + expect(mkRequest(Object.assign({}, { timeout: 6000 }, bidderRequest)).tmax).to.equal(6000) + }); }); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index 7310f736f7e..ffd6729397a 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from 'modules/adyoulikeBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; describe('Adyoulike Adapter', function () { const canonicalUrl = 'https://canonical.url/?t=%26'; @@ -707,32 +708,21 @@ describe('Adyoulike Adapter', function () { expect(payload.gdprConsent.consentRequired).to.be.null; }); - it('should add userid eids information to the request', function () { - let bidderRequest = { - 'auctionId': '1d1a030790a475', - 'bidderRequestId': '22edbae2733bf6', - 'timeout': 3000, - 'userIdAsEids': - [ - { - 'source': 'pubcid.org', - 'uids': [ - { - 'atype': 1, - 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' - } - ] - } - ] - }; - - bidderRequest.bids = bidRequestWithSinglePlacement; - - const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); + it('should add eids eids information to the request', function () { + let bidRequest = bidRequestWithSinglePlacement; + bidRequest[0].userIdAsEids = [{ + 'source': 'pubcid.org', + 'uids': [{ + 'atype': 1, + 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' + }] + }] + + const request = spec.buildRequests(bidRequest, bidderRequest); const payload = JSON.parse(request.data); - expect(payload.userId).to.exist; - expect(payload.userId).to.deep.equal(bidderRequest.userIdAsEids); + expect(payload.eids).to.exist; + expect(payload.eids).to.deep.equal(bidRequest[0].userIdAsEids); }); it('sends bid request to endpoint with single placement', function () { @@ -898,4 +888,115 @@ describe('Adyoulike Adapter', function () { expect(spec.gvlid).to.equal(259) }) }); + + describe('getUserSyncs', function () { + const syncurl_iframe = 'https://visitor.omnitagjs.com/visitor/isync?uid=19340f4f097d16f41f34fc0274981ca4'; + + const emptySync = []; + + describe('with iframe enabled', function() { + const userSyncConfig = { iframeEnabled: true }; + + it('should not add parameters if not provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should add GDPR parameters if provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=` + }]); + + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: 'foo?'}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo%3F` + }]); + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: false, consentString: 'bar'}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=0&gdpr_consent=bar` + }]); + }); + + it('should add CCPA parameters if provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, 'foo?')).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&us_privacy=foo%3F` + }]); + }); + + describe('COPPA', function() { + let sandbox; + + this.beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + this.afterEach(function() { + sandbox.restore(); + }); + + it('should add coppa parameters if provided', function() { + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + 'coppa': true + }; + return config[key]; + }); + + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&coppa=1` + }]); + }); + }); + + describe('GPP', function() { + it('should not apply if not gppConsent.gppString', function() { + const gppConsent = { gppString: '', applicableSections: [123] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should not apply if not gppConsent.applicableSections', function() { + const gppConsent = { gppString: '', applicableSections: undefined }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should not apply if empty gppConsent.applicableSections', function() { + const gppConsent = { gppString: '', applicableSections: [] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should apply if all above are available', function() { + const gppConsent = { gppString: 'foo?', applicableSections: [123] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gpp=foo%3F&gpp_sid=123` + }]); + }); + + it('should support multiple sections', function() { + const gppConsent = { gppString: 'foo', applicableSections: [123, 456] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gpp=foo&gpp_sid=123%2C456` + }]); + }); + }); + }); + + describe('with iframe disabled', function() { + const userSyncConfig = { iframeEnabled: false }; + + it('should return empty list of syncs', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal(emptySync); + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: 'foo'}, 'bar')).to.deep.equal(emptySync); + }); + }); + }); }); diff --git a/test/spec/modules/agmaAnalyticsAdapter_spec.js b/test/spec/modules/agmaAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..ba71624e3b3 --- /dev/null +++ b/test/spec/modules/agmaAnalyticsAdapter_spec.js @@ -0,0 +1,388 @@ +import adapterManager from '../../../src/adapterManager.js'; +import agmaAnalyticsAdapter, { + getTiming, + getOrtb2Data, + getPayload, +} from '../../../modules/agmaAnalyticsAdapter.js'; +import { gdprDataHandler } from '../../../src/adapterManager.js'; +import { expect } from 'chai'; +import * as events from '../../../src/events.js'; +import constants from '../../../src/constants.json'; +import { generateUUID } from '../../../src/utils.js'; +import { server } from '../../mocks/xhr.js'; +import { config } from 'src/config.js'; + +const INGEST_URL = 'https://pbc.agma-analytics.de/v1'; +const extendedKey = [ + 'auctionIds', + 'code', + 'domain', + 'extended', + 'gdprApplies', + 'gdprConsentString', + 'language', + 'ortb2', + 'pageUrl', + 'pageViewId', + 'prebidVersion', + 'referrer', + 'screenHeight', + 'screenWidth', + 'scriptVersion', + 'timestamp', + 'timezoneOffset', + 'timing', + 'triggerEvent', + 'userIdsAsEids', +]; +const nonExtendedKey = [ + 'auctionIds', + 'code', + 'domain', + 'gdprApplies', + 'ortb2', + 'pageUrl', + 'pageViewId', + 'prebidVersion', + 'scriptVersion', + 'timing', + 'triggerEvent', +]; + +describe('AGMA Analytics Adapter', () => { + let agmaConfig, sandbox, clock; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(); + sandbox.stub(events, 'getEvents').returns([]); + agmaConfig = { + options: { + code: 'test', + }, + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('configuration', () => { + it('registers itself with the adapter manager', () => { + const adapter = adapterManager.getAnalyticsAdapter('agma'); + expect(adapter).to.exist; + expect(adapter.gvlid).to.equal(1122); + }); + }); + + describe('getPayload', () => { + it('should use non extended payload with no consent info', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => null) + const payload = getPayload([generateUUID()], { + code: 'test', + }); + + expect(payload).to.have.all.keys([...nonExtendedKey, 'debug']); + }); + + it('should use non extended payload when agma is not in the TC String', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + vendorData: { + vendor: { + consents: { + 1122: false, + }, + }, + }, + })); + const payload = getPayload([generateUUID()], { + code: 'test', + }); + expect(payload).to.have.all.keys([...nonExtendedKey, 'debug']); + }); + + it('should use extended payload when agma is in the TC String', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const payload = getPayload([generateUUID()], { + code: 'test', + }); + expect(payload).to.have.all.keys([...extendedKey, 'debug']); + }); + }); + + describe('getTiming', () => { + let originalPerformance; + let originalWindowPerformanceNow; + + beforeEach(() => { + originalPerformance = global.performance; + originalWindowPerformanceNow = window.performance.now; + }); + + afterEach(() => { + global.performance = originalPerformance; + window.performance.now = originalWindowPerformanceNow; + }); + + it('returns TTFB using Timing API V2', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 100, startTime: 50 }]), + now: sinon.stub().returns(150), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 50, elapsedTime: 150 }); + }); + + it('returns TTFB using Timing API V1 when V2 is not available', () => { + global.performance = { + getEntriesByType: sinon.stub().throws(), + timing: { responseStart: 150, fetchStart: 50 }, + now: sinon.stub().returns(200), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 100, elapsedTime: 200 }); + }); + + it('returns null when Timing API is not available', () => { + global.performance = { + getEntriesByType: sinon.stub().throws(), + timing: undefined, + }; + + const result = getTiming(); + + expect(result).to.be.null; + }); + + it('returns ttfb as 0 if calculated value is negative', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 50, startTime: 150 }]), + now: sinon.stub().returns(200), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 0, elapsedTime: 200 }); + }); + + it('returns ttfb as 0 if calculated value exceeds performance.now()', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 50, startTime: 0 }]), + now: sinon.stub().returns(40), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 0, elapsedTime: 40 }); + }); + }); + + describe('getOrtb2Data', () => { + it('returns site and user from options when available', () => { + sandbox.stub(config, 'getConfig').callsFake((key) => { + return {}; + }); + + const ortb2 = { + user: 'user', + site: 'site', + }; + + const result = getOrtb2Data({ + ortb2, + }); + + expect(result).to.deep.equal(ortb2); + }); + + it('returns a combination of data from options and pGlobal.readConfig', () => { + sandbox.stub(config, 'getConfig').callsFake((key) => { + return { + ortb2: { + site: { + foo: 'bar', + }, + }, + }; + }); + + const ortb2 = { + user: 'user', + }; + const result = getOrtb2Data({ + ortb2, + }); + + expect(result).to.deep.equal({ + site: { + foo: 'bar', + }, + user: 'user', + }); + }); + }); + + describe('Event Payload', () => { + beforeEach(() => { + agmaAnalyticsAdapter.enableAnalytics({ + ...agmaConfig, + }); + server.respondWith('POST', INGEST_URL, [ + 200, + { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + '', + ]); + }); + + afterEach(() => { + agmaAnalyticsAdapter.auctionIds = []; + if (agmaAnalyticsAdapter.timer) { + clearTimeout(agmaAnalyticsAdapter.timer); + } + agmaAnalyticsAdapter.disableAnalytics(); + }); + + it('should only send once per minute', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('1'), + auction, + }); + + clock.tick(200); + + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('2'), + auction, + }); + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('3'), + auction, + }); + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('4'), + auction, + }); + + clock.tick(900); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody).to.have.all.keys(extendedKey); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(server.requests).to.have.length(1); + }); + + it('should send the extended payload with consent', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + clock.tick(1100); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody).to.have.all.keys(extendedKey); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + + it('should send the non extended payload with no explicit consent', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + })); + + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + clock.tick(1000); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + + it('should set the trigger Event', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => null); + agmaAnalyticsAdapter.disableAnalytics(); + agmaAnalyticsAdapter.enableAnalytics({ + provider: 'agma', + options: { + code: 'test', + triggerEvent: constants.EVENTS.AUCTION_END + }, + }); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + events.emit(constants.EVENTS.AUCTION_END, auction); + clock.tick(1000); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody.auctionIds).to.have.length(1); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_END); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + }); +}); diff --git a/test/spec/modules/aidemBidAdapter_spec.js b/test/spec/modules/aidemBidAdapter_spec.js index 8b401491ba0..3de348197b2 100644 --- a/test/spec/modules/aidemBidAdapter_spec.js +++ b/test/spec/modules/aidemBidAdapter_spec.js @@ -155,9 +155,9 @@ const DEFAULT_VALID_BANNER_REQUESTS = [ { adUnitCode: 'test-div', auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidId: '22c4871113f461', + bidId: '2705bfae8ea667', bidder: 'aidem', - bidderRequestId: '15246a574e859f', + bidderRequestId: '1bbb7854dfa0d8', mediaTypes: { banner: { sizes: [ @@ -171,11 +171,7 @@ const DEFAULT_VALID_BANNER_REQUESTS = [ placementId: '13144370' }, src: 'client', - ortb2Imp: { - ext: { - tid: '54a58774-7a41-494e-9aaf-fa7b79164f0c', - }, - }, + transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' } ]; @@ -183,9 +179,9 @@ const DEFAULT_VALID_VIDEO_REQUESTS = [ { adUnitCode: 'test-div', auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidId: '22c4871113f461', + bidId: '2705bfae8ea667', bidder: 'aidem', - bidderRequestId: '15246a574e859f', + bidderRequestId: '1bbb7854dfa0d8', mediaTypes: { video: { minduration: 7, @@ -200,18 +196,23 @@ const DEFAULT_VALID_VIDEO_REQUESTS = [ placementId: '13144370' }, src: 'client', - ortb2Imp: { - ext: { - tid: '54a58774-7a41-494e-9aaf-fa7b79164f0c', - } - }, + transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' } ]; const VALID_BIDDER_REQUEST = { - auctionId: '6e9b46c3-65a8-46ea-89f4-c5071110c85c', + auctionId: '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', bidderCode: 'aidem', - bidderRequestId: '170ea5d2b1d073', + bidderRequestId: '1bbb7854dfa0d8', + bids: [ + { + params: { + placementId: '13144370', + siteId: '23434', + publisherId: '7689670753' + }, + } + ], refererInfo: { page: 'test-page', domain: 'test-domain', @@ -219,182 +220,68 @@ const VALID_BIDDER_REQUEST = { }, } -// Add mediatype const SERVER_RESPONSE_BANNER = { - body: { - id: 'efa1930a-bc3e-4fd0-8368-08bc40236b4f', - bid: [ - // BANNER - { - 'id': '2e614be960ee1d', - 'impid': '2e614be960ee1d', - 'price': 7.91, - 'mediatype': 'banner', - 'adid': '24277955', - 'adm': 'creativity_banner', - 'adomain': [ - 'aidem.com' - ], - 'iurl': 'http://www.aidem.com', - 'cat': [], - 'cid': '4193561', - 'crid': '24277955', - 'w': 300, - 'h': 250, - 'ext': { - 'dspid': 85, - 'advbrandid': 1246, - 'advbrand': 'AIDEM' - } - }, - ], - cur: 'USD' - }, -} - -const SERVER_RESPONSE_VIDEO = { - body: { - id: 'efa1930a-bc3e-4fd0-8368-08bc40236b4f', - bid: [ - // VIDEO - { - 'id': '2876a29392a47c', - 'impid': '2876a29392a47c', - 'price': 7.93, - 'mediatype': 'video', - 'adid': '24277955', - 'adm': 'https://hermes.aidemsrv.com/vast-tag/cl9mzhhd502uq09l720uegb02?auction_id={{AUCTION_ID}}&cachebuster={{CACHEBUSTER}}', - 'adomain': [ - 'aidem.com' - ], - 'iurl': 'http://www.aidem.com', - 'cat': [], - 'cid': '4193561', - 'crid': '24277955', - 'w': 640, - 'h': 480, - 'ext': { - 'dspid': 85, - 'advbrandid': 1246, - 'advbrand': 'AIDEM' - } - } - ], - cur: 'USD' - }, -} - -const WIN_NOTICE_WEB = { - 'adId': '3a20ee5dc78c1e', - 'adUnitCode': 'div-gpt-ad-1460505748561-0', - 'creativeId': '24277955', - 'cpm': 1, - 'netRevenue': false, - 'adserverTargeting': { - 'hb_bidder': 'aidem', - 'hb_adid': '3a20ee5dc78c1e', - 'hb_pb': '1.00', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner', - 'hb_adomain': 'example.com' - }, - - 'auctionId': '85864730-6cbc-4e56-bc3c-a4a6596dca5b', - 'currency': [ - 'USD' - ], - 'mediaType': 'banner', - 'meta': { - 'advertiserDomains': [ - 'cloudflare.com' - ], - 'ext': {} - }, - 'size': '300x250', - 'params': [ + 'id': '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + 'seatbid': [ { - 'placementId': '13144370', - 'siteId': '23434', - 'publisherId': '7689670753' + 'bid': [ + { + 'id': 'beeswax/aidem', + 'impid': '2705bfae8ea667', + 'price': 0.00875, + 'burl': 'imp_burl', + 'adm': 'creativity_banner', + 'adid': '2:64:162:1001', + 'adomain': [ + 'aidem.com' + ], + 'cid': '64', + 'crid': 'aidem-1001', + 'cat': [], + 'w': 300, + 'h': 250, + 'mtype': 1 + } + ], + 'seat': 'aidemdsp', + 'group': 0 } ], - 'width': 300, - 'height': 250, - 'status': 'rendered', - 'transactionId': 'ce089116-4251-45c3-bdbb-3a03cb13816b', - 'ttl': 300, - 'requestTimestamp': 1666796241007, - 'responseTimestamp': 1666796241021, - metrics: { - getMetrics() { - return { - - } - } - } + 'cur': 'USD' } -const WIN_NOTICE_APP = { - 'adId': '3a20ee5dc78c1e', - 'adUnitCode': 'div-gpt-ad-1460505748561-0', - 'creativeId': '24277955', - 'cpm': 1, - 'netRevenue': false, - 'adserverTargeting': { - 'hb_bidder': 'aidem', - 'hb_adid': '3a20ee5dc78c1e', - 'hb_pb': '1.00', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner', - 'hb_adomain': 'example.com' - }, - - 'auctionId': '85864730-6cbc-4e56-bc3c-a4a6596dca5b', - 'currency': [ - 'USD' - ], - 'mediaType': 'banner', - 'meta': { - 'advertiserDomains': [ - 'cloudflare.com' - ], - 'ext': { - 'app': { - 'app_bundle': '{{APP_BUNDLE}}', - 'app_id': '{{APP_ID}}', - 'app_name': '{{APP_NAME}}', - 'app_store_url': '{{APP_STORE_URL}}', - 'inventory_source': '{{INVENTORY_SOURCE}}' - }, - 'win_notice_ext': { - 'seatid': '{{SEAT_ID}}' - } - } - }, - 'size': '300x250', - 'params': [ +const SERVER_RESPONSE_VIDEO = { + 'id': '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + 'seatbid': [ { - 'placementId': '13144370', - 'siteId': '23434', - 'publisherId': '7689670753' + 'bid': [ + { + 'id': 'beeswax/aidem', + 'impid': '2705bfae8ea667', + 'price': 0.00875, + 'burl': 'imp_burl', + 'adm': 'creativity_banner', + 'adid': '2:64:162:1001', + 'adomain': [ + 'aidem.com' + ], + 'cid': '64', + 'crid': 'aidem-1001', + 'cat': [], + 'w': 300, + 'h': 250, + 'mtype': 2 + } + ], + 'seat': 'aidemdsp', + 'group': 0 } ], - 'width': 300, - 'height': 250, - 'status': 'rendered', - 'transactionId': 'ce089116-4251-45c3-bdbb-3a03cb13816b', - 'ttl': 300, - 'requestTimestamp': 1666796241007, - 'responseTimestamp': 1666796241021, - metrics: { - getMetrics() { - return { + 'cur': 'USD' +} - } - } - } +const WIN_NOTICE = { + burl: 'burl' } const ERROR_NOTICE = { @@ -512,109 +399,95 @@ describe('Aidem adapter', () => { const requests = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); expect(requests).to.be.an('object'); expect(requests.method).to.be.a('string') - expect(requests.data).to.be.a('string') + expect(requests.data).to.be.a('object') expect(requests.options).to.be.an('object').that.have.a.property('withCredentials') }); it('should have a well formatted banner payload', () => { - const requests = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - expect(payload).to.be.a('object').that.has.all.keys( - 'id', 'imp', 'device', 'cur', 'tz', 'regs', 'site', 'environment', 'at' + const {data} = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); + expect(data).to.be.a('object').that.has.all.keys( + 'id', 'imp', 'regs', 'site', 'environment', 'at', 'test' ) - expect(payload.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_BANNER_REQUESTS.length) + expect(data.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_BANNER_REQUESTS.length) - expect(payload.imp[0]).to.be.a('object').that.has.all.keys( - 'banner', 'id', 'mediatype', 'imp_ext', 'tid', 'tagid' + expect(data.imp[0]).to.be.a('object').that.has.all.keys( + 'banner', 'id', 'tagId' ) - expect(payload.imp[0].banner).to.be.a('object').that.has.all.keys( + expect(data.imp[0].banner).to.be.a('object').that.has.all.keys( 'format', 'topframe' ) }); - it('should have a well formatted video payload', () => { - const requests = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - expect(payload).to.be.a('object').that.has.all.keys( - 'id', 'imp', 'device', 'cur', 'tz', 'regs', 'site', 'environment', 'at' - ) - expect(payload.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_VIDEO_REQUESTS.length) - - expect(payload.imp[0]).to.be.a('object').that.has.all.keys( - 'video', 'id', 'mediatype', 'imp_ext', 'tid', 'tagid' - ) - expect(payload.imp[0].video).to.be.a('object').that.has.all.keys( - 'format', 'mimes', 'minDuration', 'maxDuration', 'protocols' - ) - }); + if (FEATURES.VIDEO) { + it('should have a well formatted video payload', () => { + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); + expect(data).to.be.a('object').that.has.all.keys( + 'id', 'imp', 'regs', 'site', 'environment', 'at', 'test' + ) + expect(data.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_VIDEO_REQUESTS.length) - it('should have a well formatted bid floor payload if configured', () => { - const validBannerRequests = utils.deepClone(DEFAULT_VALID_BANNER_REQUESTS) - validBannerRequests[0].params.floor = { - value: 1.98, - currency: 'USD' - } - const requests = spec.buildRequests(validBannerRequests, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - const { floor } = payload.imp[0] - expect(floor).to.be.a('object').that.has.all.keys( - 'value', 'currency' - ) - }); + expect(data.imp[0]).to.be.a('object').that.has.all.keys( + 'video', 'id', 'tagId' + ) + expect(data.imp[0].video).to.be.a('object').that.has.all.keys( + 'mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h' + ) + }); + } it('should hav wpar keys in environment object', function () { - const requests = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - expect(payload).to.have.property('environment') - expect(payload.environment).to.be.a('object').that.have.property('wpar') - expect(payload.environment.wpar).to.be.a('object').that.has.keys('innerWidth', 'innerHeight') + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); + expect(data).to.have.property('environment') + expect(data.environment).to.be.a('object').that.have.property('wpar') + expect(data.environment.wpar).to.be.a('object').that.has.keys('innerWidth', 'innerHeight') }); }) describe('interpretResponse', () => { it('should return a valid bid array with a banner bid', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(1) - interpreted.forEach(value => { + const {data} = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST) + const bids = spec.interpretResponse({body: SERVER_RESPONSE_BANNER}, { data }) + expect(bids).to.be.a('array').that.has.lengthOf(1) + bids.forEach(value => { expect(value).to.be.a('object').that.has.all.keys( - 'ad', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'dealId' + 'ad', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'burl', 'seatBidId', 'creative_id' ) }) }); - it('should return a valid bid array with a banner bid', () => { - const response = utils.deepClone(SERVER_RESPONSE_VIDEO) - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(1) - interpreted.forEach(value => { - expect(value).to.be.a('object').that.has.all.keys( - 'vastUrl', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'dealId' - ) - }) - }); + if (FEATURES.VIDEO) { + it('should return a valid bid array with a video bid', () => { + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST) + const bids = spec.interpretResponse({body: SERVER_RESPONSE_VIDEO}, { data }) + expect(bids).to.be.a('array').that.has.lengthOf(1) + bids.forEach(value => { + expect(value).to.be.a('object').that.has.all.keys( + 'vastUrl', 'vastXml', 'playerHeight', 'playerWidth', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'burl', 'seatBidId', 'creative_id' + ) + }) + }); + } it('should return a valid bid array with netRevenue', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - response.body.bid[0].isNet = true - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(1) - expect(interpreted[0].netRevenue).to.be.true - }); - - it('should return an empty bid array if one of seatbid entry is missing price property', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - delete response.body.bid[0].price - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(0) - }); - - it('should return an empty bid array if one of seatbid entry is missing adm property', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - delete response.body.bid[0].adm - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(0) - }); + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST) + const bids = spec.interpretResponse({body: SERVER_RESPONSE_VIDEO}, { data }) + expect(bids).to.be.a('array').that.has.lengthOf(1) + expect(bids[0].netRevenue).to.be.true + }); + + // it('should return an empty bid array if one of seatbid entry is missing price property', () => { + // const response = utils.deepClone(SERVER_RESPONSE_BANNER) + // delete response.body.bid[0].price + // const interpreted = spec.interpretResponse(response) + // expect(interpreted).to.be.a('array').that.has.lengthOf(0) + // }); + + // it('should return an empty bid array if one of seatbid entry is missing adm property', () => { + // const response = utils.deepClone(SERVER_RESPONSE_BANNER) + // delete response.body.bid[0].adm + // const interpreted = spec.interpretResponse(response) + // expect(interpreted).to.be.a('array').that.has.lengthOf(0) + // }); }) describe('onBidWon', () => { @@ -622,34 +495,29 @@ describe('Aidem adapter', () => { expect(spec.onBidWon).to.exist.and.to.be.a('function') }); - it(`should send a valid bid won notice from web environment`, function () { - spec.onBidWon(WIN_NOTICE_WEB); - expect(server.requests.length).to.equal(1); - }); - - it(`should send a valid bid won notice from app environment`, function () { - spec.onBidWon(WIN_NOTICE_APP); + it(`should send a win notice`, function () { + spec.onBidWon(WIN_NOTICE); expect(server.requests.length).to.equal(1); }); }); - describe('onBidderError', () => { - it(`should exists and type function`, function () { - expect(spec.onBidderError).to.exist.and.to.be.a('function') - }); - - it(`should send a valid error notice`, function () { - spec.onBidderError({ bidderRequest: ERROR_NOTICE }) - expect(server.requests.length).to.equal(1); - const body = JSON.parse(server.requests[0].requestBody) - expect(body).to.be.a('object').that.has.all.keys('message', 'auctionId', 'bidderRequestId', 'url', 'metrics') - // const { bids } = JSON.parse(server.requests[0].requestBody) - // expect(bids).to.be.a('array').that.has.lengthOf(1) - // _each(bids, (bid) => { - // expect(bid).to.be.a('object').that.has.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'transactionId', 'metrics') - // }) - }); - }); + // describe('onBidderError', () => { + // it(`should exists and type function`, function () { + // expect(spec.onBidderError).to.exist.and.to.be.a('function') + // }); + // + // it(`should send a valid error notice`, function () { + // spec.onBidderError({ bidderRequest: ERROR_NOTICE }) + // expect(server.requests.length).to.equal(1); + // const body = JSON.parse(server.requests[0].requestBody) + // expect(body).to.be.a('object').that.has.all.keys('message', 'auctionId', 'bidderRequestId', 'url', 'metrics') + // // const { bids } = JSON.parse(server.requests[0].requestBody) + // // expect(bids).to.be.a('array').that.has.lengthOf(1) + // // _each(bids, (bid) => { + // // expect(bid).to.be.a('object').that.has.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'transactionId', 'metrics') + // // }) + // }); + // }); describe('setEndPoints', () => { it(`should exists and type function`, function () { @@ -659,64 +527,35 @@ describe('Aidem adapter', () => { it(`should not modify default endpoints`, function () { const endpoints = setEndPoints() const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) - expect(requestURL.host).to.equal('zero.aidemsrv.com') - expect(winNoticeURL.host).to.equal('zero.aidemsrv.com') - expect(timeoutNoticeURL.host).to.equal('zero.aidemsrv.com') - expect(errorNoticeURL.host).to.equal('zero.aidemsrv.com') - - expect(decodeURIComponent(requestURL.pathname)).to.equal('/bid/request') - expect(decodeURIComponent(winNoticeURL.pathname)).to.equal('/notice/win') - expect(decodeURIComponent(timeoutNoticeURL.pathname)).to.equal('/notice/timeout') - expect(decodeURIComponent(errorNoticeURL.pathname)).to.equal('/notice/error') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/prebidjs/ortb/v2.6/bid/request') }); it(`should not change request endpoint`, function () { const endpoints = setEndPoints('default') const requestURL = new URL(endpoints.request) - expect(decodeURIComponent(requestURL.pathname)).to.equal('/bid/request') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/prebidjs/ortb/v2.6/bid/request') }); it(`should change to local env`, function () { const endpoints = setEndPoints('local') const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) expect(requestURL.host).to.equal('127.0.0.1:8787') - expect(winNoticeURL.host).to.equal('127.0.0.1:8787') - expect(timeoutNoticeURL.host).to.equal('127.0.0.1:8787') - expect(errorNoticeURL.host).to.equal('127.0.0.1:8787') }); it(`should add a path prefix`, function () { const endpoints = setEndPoints('local', '/path') const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) - expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/bid/request') - expect(decodeURIComponent(winNoticeURL.pathname)).to.equal('/path/notice/win') - expect(decodeURIComponent(timeoutNoticeURL.pathname)).to.equal('/path/notice/timeout') - expect(decodeURIComponent(errorNoticeURL.pathname)).to.equal('/path/notice/error') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/prebidjs/ortb/v2.6/bid/request') }); it(`should add a path prefix and change request endpoint`, function () { const endpoints = setEndPoints('local', '/path') const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) - expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/bid/request') - expect(decodeURIComponent(winNoticeURL.pathname)).to.equal('/path/notice/win') - expect(decodeURIComponent(timeoutNoticeURL.pathname)).to.equal('/path/notice/timeout') - expect(decodeURIComponent(errorNoticeURL.pathname)).to.equal('/path/notice/error') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/prebidjs/ortb/v2.6/bid/request') }); }); @@ -758,8 +597,7 @@ describe('Aidem adapter', () => { coppa: true }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.coppa_applies).to.equal(true) + expect(data.regs.coppa_applies).to.equal(true) }); it(`should set gdpr to true`, function () { @@ -771,8 +609,7 @@ describe('Aidem adapter', () => { } }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.gdpr_applies).to.equal(true) + expect(data.regs.gdpr_applies).to.equal(true) }); it(`should set usp_consent string`, function () { @@ -789,8 +626,7 @@ describe('Aidem adapter', () => { } }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.us_privacy).to.equal('1YYY') + expect(data.regs.us_privacy).to.equal('1YYY') }); it(`should not set usp_consent string`, function () { @@ -807,8 +643,7 @@ describe('Aidem adapter', () => { } }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.us_privacy).to.undefined + expect(data.regs.us_privacy).to.undefined }); }); }); diff --git a/test/spec/modules/alkimiBidAdapter_spec.js b/test/spec/modules/alkimiBidAdapter_spec.js index a396e5b8139..90a9e409e69 100644 --- a/test/spec/modules/alkimiBidAdapter_spec.js +++ b/test/spec/modules/alkimiBidAdapter_spec.js @@ -14,8 +14,7 @@ const REQUEST = { }, 'params': { bidFloor: 0.1, - token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', - pos: 7 + token: 'e64782a4-8e68-4c38-965b-80ccf115d46f' }, 'userIdAsEids': [{ 'source': 'criteo.com', @@ -69,7 +68,7 @@ const BIDDER_VIDEO_RESPONSE = { 'ttl': 200, 'creativeId': 2, 'netRevenue': true, - 'winUrl': 'http://test.com', + 'winUrl': 'http://test.com?price=${AUCTION_PRICE}', 'mediaType': 'video', 'adomain': ['test.com'] }] @@ -96,10 +95,6 @@ describe('alkimiBidAdapter', function () { delete bid.params.token expect(spec.isBidRequestValid(bid)).to.equal(false) - bid = Object.assign({}, REQUEST) - delete bid.params.bidFloor - expect(spec.isBidRequestValid(bid)).to.equal(false) - bid = Object.assign({}, REQUEST) delete bid.params expect(spec.isBidRequestValid(bid)).to.equal(false) @@ -109,7 +104,6 @@ describe('alkimiBidAdapter', function () { describe('buildRequests', function () { let bidRequests = [REQUEST] let requestData = { - auctionId: '123', refererInfo: { page: 'http://test.com/path.html' }, @@ -118,7 +112,15 @@ describe('alkimiBidAdapter', function () { vendorData: {}, gdprApplies: true }, - uspConsent: 'uspConsent' + uspConsent: 'uspConsent', + ortb2: { + site: { + keywords: 'test1, test2' + }, + at: 2, + bcat: ['BSW1', 'BSW2'], + wseat: ['16', '165'] + } } const bidderRequest = spec.buildRequests(bidRequests, requestData) @@ -137,13 +139,14 @@ describe('alkimiBidAdapter', function () { it('sends bid request to ENDPOINT via POST', function () { expect(bidderRequest.method).to.equal('POST') - expect(bidderRequest.data.requestId).to.equal('123') + expect(bidderRequest.data.requestId).to.not.equal(undefined) expect(bidderRequest.data.referer).to.equal('http://test.com/path.html') expect(bidderRequest.data.schain).to.deep.contains({ ver: '1.0', complete: 1, nodes: [{ asi: 'alkimi-onboarding.com', sid: '00001', hp: 1 }] }) - expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', pos: 7, bidFloor: 0.1, sizes: [{width: 300, height: 250}], playerSizes: [], impMediaTypes: ['Banner'], adUnitCode: 'bannerAdUnitCode' }) + expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', bidFloor: 0.1, sizes: [{ width: 300, height: 250 }], playerSizes: [], impMediaTypes: ['Banner'], adUnitCode: 'bannerAdUnitCode', instl: undefined, exp: undefined, banner: { sizes: [[300, 250]] }, video: undefined }) expect(bidderRequest.data.signRequest.randomUUID).to.equal(undefined) expect(bidderRequest.data.bidIds).to.deep.contains('456') expect(bidderRequest.data.signature).to.equal(undefined) + expect(bidderRequest.data.ortb2).to.deep.contains({ at: 2, wseat: ['16', '165'], bcat: ['BSW1', 'BSW2'], site: { keywords: 'test1, test2' }, }) expect(bidderRequest.options.customHeaders).to.deep.equal({ 'Rtb-Direct': true }) expect(bidderRequest.options.contentType).to.equal('application/json') expect(bidderRequest.url).to.equal(ENDPOINT) @@ -192,9 +195,9 @@ describe('alkimiBidAdapter', function () { expect(result[0]).to.have.property('ttl').equal(200) expect(result[0]).to.have.property('creativeId').equal(2) expect(result[0]).to.have.property('netRevenue').equal(true) - expect(result[0]).to.have.property('winUrl').equal('http://test.com') + expect(result[0]).to.have.property('winUrl').equal('http://test.com?price=${AUCTION_PRICE}') expect(result[0]).to.have.property('mediaType').equal('video') - expect(result[0]).to.have.property('vastXml').equal('vast') + expect(result[0]).to.have.property('vastUrl').equal('http://test.com?price=800.4') expect(result[0].meta).to.exist.property('advertiserDomains') expect(result[0].meta).to.have.property('advertiserDomains').lengthOf(1) }) diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js index 6f69e57d8bc..984c443344d 100644 --- a/test/spec/modules/amxBidAdapter_spec.js +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -39,7 +39,9 @@ const sampleBidderRequest = { gppString: 'example', applicableSections: 'example' }, - auctionId: utils.getUniqueIdentifierStr(), + + auctionId: null, + uspConsent: '1YYY', refererInfo: { reachedTop: true, @@ -90,7 +92,8 @@ const sampleBidRequestBase = { adUnitCode: 'div-gpt-ad-example', transactionId: utils.getUniqueIdentifierStr(), bidId: sampleRequestId, - auctionId: utils.getUniqueIdentifierStr(), + + auctionId: null, }; const schainConfig = { @@ -494,7 +497,7 @@ describe('AmxBidAdapter', () => { it('can build a video request', () => { const { data } = spec.buildRequests( - [{...sampleBidRequestVideo, params: { ...sampleBidRequestVideo.params, adUnitId: 'custom-auid' }}], + [{ ...sampleBidRequestVideo, params: { ...sampleBidRequestVideo.params, adUnitId: 'custom-auid' } }], sampleBidderRequest ); expect(Object.keys(data.m).length).to.equal(1); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 13ef31a68d4..193e8aa64f8 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -1093,7 +1093,18 @@ describe('AppNexusAdapter', function () { expect(payload.tags[0].use_pmt_rule).to.equal(true); }); - it('should add gpid to the request', function () { + it('should add preferred gpid to the request', function () { + let testGpid = '/12345/my-gpt-tag-0'; + let bidRequest = deepClone(bidRequests[0]); + bidRequest.ortb2Imp = { ext: { gpid: testGpid } }; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].gpid).to.exist.and.equal(testGpid) + }); + + it('should add backup gpid to the request', function () { let testGpid = '/12345/my-gpt-tag-0'; let bidRequest = deepClone(bidRequests[0]); bidRequest.ortb2Imp = { ext: { data: { pbadslot: testGpid } } }; diff --git a/test/spec/modules/asteriobidAnalyticsAdapter_spec.js b/test/spec/modules/asteriobidAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..9be6c1dedac --- /dev/null +++ b/test/spec/modules/asteriobidAnalyticsAdapter_spec.js @@ -0,0 +1,151 @@ +import asteriobidAnalytics, {storage} from 'modules/asteriobidAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +import * as utils from 'src/utils.js'; +import {expectEvents} from '../../helpers/analytics.js'; + +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('AsterioBid Analytics Adapter', function () { + let bidWonEvent = { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'adId': '1ebb82ec35375e', + 'mediaType': 'banner', + 'cpm': 0.5, + 'requestId': '1582271863760569973', + 'creative_id': '96846035', + 'creativeId': '96846035', + 'ttl': 60, + 'currency': 'USD', + 'netRevenue': true, + 'auctionId': '9c7b70b9-b6ab-4439-9e71-b7b382797c18', + 'responseTimestamp': 1537521629657, + 'requestTimestamp': 1537521629331, + 'bidder': 'appnexus', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'timeToRespond': 326, + 'size': '300x250', + 'status': 'rendered', + 'eventType': 'bidWon', + 'ad': 'some ad', + 'adUrl': 'ad url' + }; + + describe('AsterioBid Analytic tests', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + }); + + afterEach(function () { + asteriobidAnalytics.disableAnalytics(); + events.getEvents.restore(); + }); + + it('support custom endpoint', function () { + let custom_url = 'custom url'; + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + url: custom_url, + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + expect(asteriobidAnalytics.getOptions().url).to.equal(custom_url); + }); + + it('bid won event', function() { + let bundleId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: bundleId + } + }); + + events.emit(constants.EVENTS.BID_WON, bidWonEvent); + asteriobidAnalytics.flush(); + + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('https://endpt.asteriobid.com/endpoint'); + expect(server.requests[0].requestBody.substring(0, 2)).to.equal('1:'); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + expect(pmEvents.pageViewId).to.exist; + expect(pmEvents.bundleId).to.equal(bundleId); + expect(pmEvents.ver).to.equal(1); + expect(pmEvents.events.length).to.equal(1); + expect(pmEvents.events[0].eventType).to.equal('bidWon'); + expect(pmEvents.events[0].ad).to.be.undefined; + expect(pmEvents.events[0].adUrl).to.be.undefined; + }); + + it('track event without errors', function () { + sinon.spy(asteriobidAnalytics, 'track'); + + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + expectEvents().to.beTrackedBy(asteriobidAnalytics.track); + }); + }); + + describe('build utm tag data', function () { + let getDataFromLocalStorageStub; + this.timeout(4000) + beforeEach(function () { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorageStub.withArgs('pm_utm_source').returns('utm_source'); + getDataFromLocalStorageStub.withArgs('pm_utm_medium').returns('utm_medium'); + getDataFromLocalStorageStub.withArgs('pm_utm_campaign').returns('utm_camp'); + getDataFromLocalStorageStub.withArgs('pm_utm_term').returns(''); + getDataFromLocalStorageStub.withArgs('pm_utm_content').returns(''); + }); + afterEach(function () { + getDataFromLocalStorageStub.restore(); + asteriobidAnalytics.disableAnalytics() + }); + it('should build utm data from local storage', function () { + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.utmTags.utm_source).to.equal('utm_source'); + expect(pmEvents.utmTags.utm_medium).to.equal('utm_medium'); + expect(pmEvents.utmTags.utm_campaign).to.equal('utm_camp'); + expect(pmEvents.utmTags.utm_term).to.equal(''); + expect(pmEvents.utmTags.utm_content).to.equal(''); + }); + }); + + describe('build page info', function () { + afterEach(function () { + asteriobidAnalytics.disableAnalytics() + }); + it('should build page info', function () { + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.pageInfo.domain).to.equal(window.location.hostname); + expect(pmEvents.pageInfo.referrerDomain).to.equal(utils.parseUrl(document.referrer).hostname); + }); + }); +}); diff --git a/test/spec/modules/automatadAnalyticsAdapter_spec.js b/test/spec/modules/automatadAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..e591f7e8e95 --- /dev/null +++ b/test/spec/modules/automatadAnalyticsAdapter_spec.js @@ -0,0 +1,533 @@ +import * as events from 'src/events'; +import * as utils from 'src/utils.js'; + +import spec, {self as exports} from 'modules/automatadAnalyticsAdapter.js'; + +import CONSTANTS from 'src/constants.json'; +import { expect } from 'chai'; + +const { + AUCTION_DEBUG, + BID_REQUESTED, + BID_REJECTED, + AUCTION_INIT, + BIDDER_DONE, + BID_RESPONSE, + BID_TIMEOUT, + BID_WON, + NO_BID +} = CONSTANTS.EVENTS + +const CONFIG_WITH_DEBUG = { + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + }, + includeEvents: [AUCTION_DEBUG, AUCTION_INIT, BIDDER_DONE, BID_RESPONSE, BID_TIMEOUT, NO_BID, BID_WON, BID_REQUESTED, BID_REJECTED] +} + +describe('Automatad Analytics Adapter', () => { + var sandbox, clock; + + describe('Adapter Setup Configuration', () => { + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(utils, 'logMessage') + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logError'); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('Should log error and return false if nothing is passed as the param in the enable analytics call', () => { + spec.enableAnalytics() + + expect(utils.logError.called).to.equal(true) + }); + + it('Should log error and return false if object type is not passed as the param in the enable analytics call', () => { + spec.enableAnalytics('hello world') + + expect(utils.logError.called).to.equal(true) + }); + + it('Should log error and return false if options is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter' + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should log error and return false if pub id is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + siteID: '230' + } + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should log error and return false if pub id is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230' + } + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should successfully configure the adapter and set global log debug messages flag to false', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421', + logDebug: false + } + }); + expect(utils.logError.called).to.equal(false) + expect(utils.logMessage.called).to.equal(true) + spec.disableAnalytics(); + }); + it('Should successfully configure the adapter and set global log debug messages flag to true', () => { + sandbox.stub(exports, 'initializeQueue').callsFake(() => {}); + sandbox.stub(exports, 'addGPTHandlers').callsFake(() => {}); + const config = { + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '410', + logDebug: true + } + } + + spec.enableAnalytics(config) + expect(utils.logError.called).to.equal(false) + expect(exports.initializeQueue.called).to.equal(true) + expect(exports.addGPTHandlers.called).to.equal(true) + expect(utils.logMessage.called).to.equal(true) + spec.disableAnalytics(); + }); + }); + + describe('Behaviour of the adapter when the sdk has loaded', () => { + before(() => { + spec.enableAnalytics(CONFIG_WITH_DEBUG); + const obj = { + auctionInitHandler: (args) => {}, + bidResponseHandler: (args) => {}, + bidderDoneHandler: (args) => {}, + bidWonHandler: (args) => {}, + noBidHandler: (args) => {}, + auctionDebugHandler: (args) => {}, + bidderTimeoutHandler: (args) => {}, + bidRequestedHandler: (args) => {}, + bidRejectedHandler: (args) => {} + } + + global.window.atmtdAnalytics = obj + + Object.keys(obj).forEach((fn) => sandbox.spy(global.window.atmtdAnalytics, fn)) + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logMessage'); + sandbox.stub(utils, 'logError'); + }); + afterEach(() => { + sandbox.restore(); + }); + after(() => { + global.window.atmtdAnalytics = undefined; + spec.disableAnalytics(); + }) + + it('Should call the auctionInitHandler when the auction init event is fired', () => { + events.emit(AUCTION_INIT, {type: AUCTION_INIT}) + expect(global.window.atmtdAnalytics.auctionInitHandler.called).to.equal(true) + }); + + it('Should call the bidRequested when the bidRequested event is fired', () => { + events.emit(BID_REQUESTED, {type: BID_REQUESTED}) + expect(global.window.atmtdAnalytics.bidRequestedHandler.called).to.equal(true) + }); + + it('Should call the bidRejected when the bidRejected event is fired', () => { + events.emit(BID_REJECTED, {type: BID_REJECTED}) + expect(global.window.atmtdAnalytics.bidRejectedHandler.called).to.equal(true) + }); + + it('Should call the bidResponseHandler when the bidResponse event is fired', () => { + events.emit(BID_RESPONSE, {type: BID_RESPONSE}) + expect(global.window.atmtdAnalytics.bidResponseHandler.called).to.equal(true) + }); + + it('Should call the bidderDoneHandler when the bidderDone event is fired', () => { + events.emit(BIDDER_DONE, {type: BIDDER_DONE}) + expect(global.window.atmtdAnalytics.bidderDoneHandler.called).to.equal(true) + }); + + it('Should call the bidWonHandler when the bidWon event is fired', () => { + events.emit(BID_WON, {type: BID_WON}) + expect(global.window.atmtdAnalytics.bidWonHandler.called).to.equal(true) + }); + + it('Should call the noBidHandler when the noBid event is fired', () => { + events.emit(NO_BID, {type: NO_BID}) + expect(global.window.atmtdAnalytics.noBidHandler.called).to.equal(true) + }); + + it('Should call the bidTimeoutHandler when the bidTimeout event is fired', () => { + events.emit(BID_TIMEOUT, {type: BID_TIMEOUT}) + expect(global.window.atmtdAnalytics.bidderTimeoutHandler.called).to.equal(true) + }); + + it('Should call the auctionDebugHandler when the auctionDebug event is fired', () => { + events.emit(AUCTION_DEBUG, {type: AUCTION_DEBUG}) + expect(global.window.atmtdAnalytics.auctionDebugHandler.called).to.equal(true) + }); + }); + + describe('Behaviour of the adapter when the SDK has not loaded', () => { + before(() => { + spec.enableAnalytics(CONFIG_WITH_DEBUG); + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logMessage'); + sandbox.stub(utils, 'logError'); + + global.window.atmtdAnalytics = undefined + exports.__atmtdAnalyticsQueue.length = 0 + sandbox.stub(exports.__atmtdAnalyticsQueue, 'push').callsFake((args) => { + Array.prototype.push.apply(exports.__atmtdAnalyticsQueue, [args]); + }) + }); + afterEach(() => { + sandbox.restore(); + }); + after(() => { + spec.disableAnalytics(); + }) + + it('Should push to the que when the auctionInit event is fired', () => { + events.emit(AUCTION_INIT, {type: AUCTION_INIT}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(AUCTION_INIT) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(AUCTION_INIT) + }); + + it('Should push to the que when the bidResponse event is fired', () => { + events.emit(BID_RESPONSE, {type: BID_RESPONSE}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_RESPONSE) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_RESPONSE) + }); + + it('Should push to the que when the bidRequested event is fired', () => { + events.emit(BID_REQUESTED, {type: BID_REQUESTED}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_REQUESTED) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_REQUESTED) + }); + + it('Should push to the que when the bidRejected event is fired', () => { + events.emit(BID_REJECTED, {type: BID_REJECTED}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_REJECTED) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_REJECTED) + }); + + it('Should push to the que when the bidderDone event is fired', () => { + events.emit(BIDDER_DONE, {type: BIDDER_DONE}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BIDDER_DONE) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BIDDER_DONE) + }); + + it('Should push to the que when the bidWon event is fired', () => { + events.emit(BID_WON, {type: BID_WON}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_WON) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_WON) + }); + + it('Should push to the que when the noBid event is fired', () => { + events.emit(NO_BID, {type: NO_BID}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(NO_BID) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(NO_BID) + }); + + it('Should push to the que when the auctionDebug is fired', () => { + events.emit(AUCTION_DEBUG, {type: AUCTION_DEBUG}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(AUCTION_DEBUG) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(AUCTION_DEBUG) + }); + + it('Should push to the que when the bidderTimeout event is fired', () => { + events.emit(BID_TIMEOUT, {type: BID_TIMEOUT}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_TIMEOUT) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_TIMEOUT) + }); + }); + + describe('Process Events from Que when SDK still has not loaded', () => { + before(() => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + } + }); + global.window.atmtdAnalytics = undefined + + sandbox.stub(exports.__atmtdAnalyticsQueue, 'push').callsFake((args) => { + Array.prototype.push.apply(exports.__atmtdAnalyticsQueue, [args]); + }) + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.spy(exports, 'prettyLog') + sandbox.spy(exports, 'processEvents') + + clock = sandbox.useFakeTimers(); + exports.__atmtdAnalyticsQueue.length = 0 + }); + afterEach(() => { + sandbox.restore(); + exports.queuePointer = 0; + exports.retryCount = 0; + exports.__atmtdAnalyticsQueue = [] + spec.disableAnalytics(); + }) + + it('Should retry processing auctionInit in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [[AUCTION_INIT, {type: AUCTION_INIT}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + + it('Should retry processing slotRenderEnded in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [['slotRenderEnded', {type: 'slotRenderEnded'}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + + it('Should retry processing impressionViewable in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [['impressionViewable', {type: 'impressionViewable'}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + }); + + describe('Process Events from Que when SDK has loaded', () => { + before(() => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + } + }); + sandbox = sinon.createSandbox(); + sandbox.reset() + const obj = { + auctionInitHandler: (args) => {}, + bidResponseHandler: (args) => {}, + bidderDoneHandler: (args) => {}, + bidWonHandler: (args) => {}, + noBidHandler: (args) => {}, + auctionDebugHandler: (args) => {}, + bidderTimeoutHandler: (args) => {}, + impressionViewableHandler: (args) => {}, + slotRenderEndedGPTHandler: (args) => {}, + bidRequestedHandler: (args) => {}, + bidRejectedHandler: (args) => {} + } + + global.window.atmtdAnalytics = obj; + + Object.keys(obj).forEach((fn) => sandbox.spy(global.window.atmtdAnalytics, fn)) + sandbox.stub(events, 'getEvents').returns([]); + sandbox.spy(exports, 'prettyLog') + exports.retryCount = 0; + exports.queuePointer = 0; + exports.__atmtdAnalyticsQueue = [ + [AUCTION_INIT, {type: AUCTION_INIT}], + [BID_RESPONSE, {type: BID_RESPONSE}], + [BID_REQUESTED, {type: BID_REQUESTED}], + [BID_REJECTED, {type: BID_REJECTED}], + [NO_BID, {type: NO_BID}], + [BID_WON, {type: BID_WON}], + [BIDDER_DONE, {type: BIDDER_DONE}], + [AUCTION_DEBUG, {type: AUCTION_DEBUG}], + [BID_TIMEOUT, {type: BID_TIMEOUT}], + ['slotRenderEnded', {type: 'slotRenderEnded'}], + ['impressionViewable', {type: 'impressionViewable'}] + ] + }); + after(() => { + sandbox.restore(); + spec.disableAnalytics(); + }) + + it('Should make calls to appropriate SDK event handlers', () => { + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.retryCount).to.equal(0) + expect(exports.prettyLog.callCount).to.equal(1) + expect(exports.queuePointer).to.equal(exports.__atmtdAnalyticsQueue.length) + expect(global.window.atmtdAnalytics.auctionInitHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidResponseHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidRejectedHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidRequestedHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.noBidHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidWonHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.auctionDebugHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidderTimeoutHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidderDoneHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.slotRenderEndedGPTHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.impressionViewableHandler.calledOnce).to.equal(true) + }) + }); + + describe('Prettylog fn tests', () => { + beforeEach(() => { + sandbox = sinon.createSandbox() + sandbox.spy(utils, 'logInfo') + sandbox.spy(utils, 'logError') + exports.isLoggingEnabled = true + }) + + afterEach(() => { + sandbox.restore() + }) + + it('Should call logMessage once in normal mode', () => { + exports.prettyLog('status', 'Hello world') + expect(utils.logInfo.callCount).to.equal(1) + }) + + it('Should call logMessage twice in group mode and have the cb called', () => { + const spy = sandbox.spy() + exports.prettyLog('status', 'Hello world', true, spy) + expect(utils.logInfo.callCount).to.equal(2) + expect(spy.called).to.equal(true) + }) + + it('Should call logMessage twice in group mode and have the cb which throws an error', () => { + const spy = sandbox.stub().throws() + exports.prettyLog('status', 'Hello world', true, spy) + expect(utils.logInfo.callCount).to.equal(2) + expect(utils.logError.called).to.equal(true) + }) + }); +}); diff --git a/test/spec/modules/axisBidAdapter_spec.js b/test/spec/modules/axisBidAdapter_spec.js new file mode 100644 index 00000000000..083f05f5c0a --- /dev/null +++ b/test/spec/modules/axisBidAdapter_spec.js @@ -0,0 +1,414 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/axisBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'axis' + +describe('AxisBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]], + pos: 1 + } + }, + params: { + integration: '000000', + token: '000000' + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60, + pos: 1 + } + }, + params: { + integration: '000000', + token: '000000' + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + integration: '000000', + token: '000000' + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + ortb2: { + site: { + cat: ['IAB24'] + } + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://prebid.axis-marketplace.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'iabCat', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.iabCat).to.have.lengthOf(1); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.integration).to.be.a('string'); + expect(placement.token).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + expect(placement.pos).to.be.within(0, 7); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + expect(placement.pos).to.be.within(0, 7); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + width: 300, + height: 250, + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta', 'width', 'height'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.axis-marketplace.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.axis-marketplace.com/image?pbjs=1&ccpa=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index 4e30b822a61..c0994985aae 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { spec, VIDEO_ENDPOINT, BANNER_ENDPOINT, OUTSTREAM_SRC, DEFAULT_MIMES } from 'modules/beachfrontBidAdapter.js'; -import { config } from 'src/config.js'; -import { parseUrl, deepAccess } from 'src/utils.js'; +import { parseUrl } from 'src/utils.js'; describe('BeachfrontAdapter', function () { let bidRequests; @@ -296,6 +295,23 @@ describe('BeachfrontAdapter', function () { expect(data.user.ext.consent).to.equal(consentString); }); + it('must add GPP consent data to the request', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { video: {} }; + const gppString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + const applicableSections = [1, 2, 3]; + const bidderRequest = { + gppConsent: { + gppString, + applicableSections + } + }; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.regs.gpp).to.equal(gppString); + expect(data.regs.gpp_sid).to.deep.equal(applicableSections); + }); + it('must add schain data to the request', () => { const schain = { ver: '1.0', @@ -517,6 +533,23 @@ describe('BeachfrontAdapter', function () { expect(data.gdprConsent).to.equal(consentString); }); + it('must add GPP consent data to the request', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + const gppString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + const applicableSections = [1, 2, 3]; + const bidderRequest = { + gppConsent: { + gppString, + applicableSections + } + }; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.gpp).to.equal(gppString); + expect(data.gppSid).to.deep.equal(applicableSections); + }); + it('must add schain data to the request', () => { const schain = { ver: '1.0', diff --git a/test/spec/modules/beopBidAdapter_spec.js b/test/spec/modules/beopBidAdapter_spec.js index 22c48c2d182..c77e304e539 100644 --- a/test/spec/modules/beopBidAdapter_spec.js +++ b/test/spec/modules/beopBidAdapter_spec.js @@ -130,15 +130,15 @@ describe('BeOp Bid Adapter tests', () => { expect(payload.url).to.exist; // check that the protocol is added correctly expect(payload.url).to.equal('http://test.te'); - expect(payload.psegs).to.not.exist; }); - it('should call the endpoint with psegs data if any', function () { + it('should call the endpoint with psegs and bpsegs (stringified) data if any or [] if none', function () { let bidderRequest = { 'ortb2': { 'user': { 'ext': { + 'bpsegs': ['axed', 'axec', 1234], 'data': { 'permutive': [1234, 5678, 910] } @@ -154,6 +154,22 @@ describe('BeOp Bid Adapter tests', () => { expect(payload.psegs).to.include(5678); expect(payload.psegs).to.include(910); expect(payload.psegs).to.not.include(1); + expect(payload.bpsegs).to.exist; + expect(payload.bpsegs).to.include('axed'); + expect(payload.bpsegs).to.include('axec'); + expect(payload.bpsegs).to.include('1234'); + + let bidderRequest2 = + { + 'ortb2': {} + }; + + const request2 = spec.buildRequests(bidRequests, bidderRequest2); + const payload2 = JSON.parse(request2.data); + expect(payload2.psegs).to.exist; + expect(payload2.psegs).to.be.empty; + expect(payload2.bpsegs).to.exist; + expect(payload2.bpsegs).to.be.empty; }); it('should not prepend the protocol in page url if already present', function () { diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index a822d86f852..2d2e51abbe1 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -245,18 +245,31 @@ describe('#bidViewability', function() { let logWinningBidNotFoundSpy; let callBidViewableBidderSpy; let winningBidsArray; + let callBidBillableBidderSpy; + let adUnits = [ + { + 'code': 'abc123', + 'bids': [ + { + 'bidder': 'pubmatic' + } + ] + } + ]; beforeEach(function() { sandbox = sinon.sandbox.create(); triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); eventsEmitSpy = sandbox.spy(events, ['emit']); callBidViewableBidderSpy = sandbox.spy(adapterManager, ['callBidViewableBidder']); + callBidBillableBidderSpy = sandbox.spy(adapterManager, ['callBidBillableBidder']); // mocking winningBidsArray winningBidsArray = []; sandbox.stub(prebidGlobal, 'getGlobal').returns({ getAllWinningBids: function (number) { return winningBidsArray; - } + }, + adUnits }); }); @@ -293,5 +306,23 @@ describe('#bidViewability', function() { // CONSTANTS.EVENTS.BID_VIEWABLE is NOT triggered expect(eventsEmitSpy.callCount).to.equal(0); }); + + it('should call the callBidBillableBidder function if the viewable bid is associated with an ad unit with deferBilling set to true', function() { + let moduleConfig = {}; + const deferredBillingAdUnit = { + 'code': '/harshad/Jan/2021/', + 'deferBilling': true, + 'bids': [ + { + 'bidder': 'pubmatic' + } + ] + }; + adUnits.push(deferredBillingAdUnit); + winningBidsArray.push(PBJS_WINNING_BID); + bidViewability.impressionViewableHandler(moduleConfig, GPT_SLOT, null); + expect(callBidBillableBidderSpy.callCount).to.equal(1); + sinon.assert.calledWith(callBidBillableBidderSpy, PBJS_WINNING_BID); + }); }); }); diff --git a/test/spec/modules/bliinkBidAdapter_spec.js b/test/spec/modules/bliinkBidAdapter_spec.js index 8e96bd76940..3db97a17d88 100644 --- a/test/spec/modules/bliinkBidAdapter_spec.js +++ b/test/spec/modules/bliinkBidAdapter_spec.js @@ -1,5 +1,16 @@ -import { expect } from 'chai' -import { spec, buildBid, BLIINK_ENDPOINT_ENGINE, getMetaList, BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME } from 'modules/bliinkBidAdapter.js' +import { expect } from 'chai'; +import { + spec, + buildBid, + BLIINK_ENDPOINT_ENGINE, + getMetaList, + BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME, + getEffectiveConnectionType, + getUserIds, + getDomLoadingDuration, + GVL_ID, +} from 'modules/bliinkBidAdapter.js'; +import { config } from 'src/config.js'; /** * @description Mockup bidRequest @@ -20,6 +31,9 @@ import { spec, buildBid, BLIINK_ENDPOINT_ENGINE, getMetaList, BLIINK_ENDPOINT_CO * crumbs: {pubcid: string}, * ortb2Imp: {ext: {data: {pbadslot: string}}}}} */ + +const connectionType = getEffectiveConnectionType(); +const domLoadingDuration = getDomLoadingDuration().toString(); const getConfigBid = (placement) => { return { adUnitCode: '/19968336/test', @@ -31,31 +45,33 @@ const getConfigBid = (placement) => { bidderRequestsCount: 1, bidderWinsCount: 0, crumbs: { - pubcid: '55ffadc5-051f-428d-8ecc-dc585e0bde0d' + pubcid: '55ffadc5-051f-428d-8ecc-dc585e0bde0d', }, sizes: [[300, 250]], mediaTypes: { banner: { - sizes: [ - [300, 250] - ] - } + sizes: [[300, 250]], + }, }, ortb2Imp: { ext: { data: { - pbadslot: '/19968336/test' - } - } + pbadslot: '/19968336/test', + }, + }, }, + domLoadingDuration, + ect: connectionType, params: { placement: placement, - tagId: '14f30eca-85d2-11e8-9eed-0242ac120007' + tagId: '14f30eca-85d2-11e8-9eed-0242ac120007', + videoUrl: 'https://www.example.com/advideo.mp4', + imageUrl: 'https://www.example.com/adimage.jpg', }, src: 'client', - transactionId: 'cc6678c4-9746-4082-b9e2-d8065d078ebf' - } -} + transactionId: 'cc6678c4-9746-4082-b9e2-d8065d078ebf', + }; +}; const getConfigBannerBid = () => { return { creative: { @@ -76,14 +92,13 @@ const getConfigBannerBid = () => { transaction_id: '2def0c5b2a7f6e', }, currency: 'EUR', - } -} + }; +}; const getConfigVideoBid = () => { return { creative: { video: { - content: - '', + content: '', height: 250, width: 300, }, @@ -99,8 +114,8 @@ const getConfigVideoBid = () => { transaction_id: '2def0c5b2a7f6e', }, currency: 'EUR', - } -} + }; +}; /** * @description Mockup response from engine.bliink.io/xxxx @@ -119,7 +134,7 @@ const getConfigVideoBid = () => { * } * } * } -* } + * } */ const getConfigCreative = () => { return { @@ -132,8 +147,8 @@ const getConfigCreative = () => { height: 250, ttl: 300, netRevenue: true, - } -} + }; +}; const getConfigCreativeVideo = (isNoVast) => { return { @@ -147,8 +162,8 @@ const getConfigCreativeVideo = (isNoVast) => { height: 250, ttl: 300, netRevenue: true, - } -} + }; +}; /** * @description Mockup BuildRequest function @@ -166,19 +181,19 @@ const getConfigBuildRequest = (placement) => { reachedTop: true, page: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', }, - } + }; if (!placement) { - return buildRequest + return buildRequest; } return Object.assign(buildRequest, { params: { bids: [getConfigBid(placement)], - placement: placement + placement: placement, }, - }) -} + }); +}; /** * @description Mockup response from API @@ -189,8 +204,8 @@ const getConfigInterpretResponse = (noAd = false) => { if (noAd) { return { message: 'invalid tag', - mode: 'no-ad' - } + mode: 'no-ad', + }; } return { @@ -198,11 +213,12 @@ const getConfigInterpretResponse = (noAd = false) => { ...getConfigCreative(), mode: 'ad', transactionId: '2def0c5b2a7f6e', - token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjgxNzA4MzEsImlhdCI6MTYyNzU2NjAzMSwiaXNzIjoiYmxpaW5rIiwiZGF0YSI6eyJ0eXBlIjoiYWQtc2VydmVyIiwidHJhbnNhY3Rpb25JZCI6IjM1YmU1NDNjLTNkZTQtNGQ1Yy04N2NjLWIzYzEyOGZiYzU0MCIsIm5ldHdvcmtJZCI6MjEsInNpdGVJZCI6NTksInRhZ0lkIjo1OSwiY29va2llSWQiOiJjNGU4MWVhOS1jMjhmLTQwZDItODY1ZC1hNjQzZjE1OTcyZjUiLCJldmVudElkIjozLCJ0YXJnZXRpbmciOnsicGxhdGZvcm0iOiJXZWJzaXRlIiwiaXAiOiI3OC4xMjIuNzUuNzIiLCJ0aW1lIjoxNjI3NTY2MDMxLCJsb2NhdGlvbiI6eyJsYXRpdHVkZSI6NDguOTczOSwibG9uZ2l0dWRlIjozLjMxMTMsInJlZ2lvbiI6IkhERiIsImNvdW50cnkiOiJGUiIsImNpdHkiOiJTYXVsY2hlcnkiLCJ6aXBDb2RlIjoiMDIzMTAiLCJkZXBhcnRtZW50IjoiMDIifSwiY2l0eSI6IlNhdWxjaGVyeSIsImNvdW50cnkiOiJGUiIsImRldmljZU9zIjoibWFjT1MiLCJkZXZpY2VQbGF0Zm9ybSI6IldlYnNpdGUiLCJyYXdVc2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoTWFjaW50b3NoOyBJbnRlbCBNYWMgT1MgWCAxMF8xNV83KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvOTEuMC40NDcyLjEyNCBTYWZhcmkvNTM3LjM2In0sImdkcHIiOnsiaGFzQ29uc2VudCI6dHJ1ZX0sIndpbiI6ZmFsc2UsImFkSWQiOjU2NDgsImFkdmVydGlzZXJJZCI6MSwiY2FtcGFpZ25JZCI6MSwiY3JlYXRpdmVJZCI6MjgyNSwiZXJyb3IiOmZhbHNlfX0.-UefQH4G0k-RJGemBYffs-KL7EEwma2Wuwgk2xnpij8' + token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjgxNzA4MzEsImlhdCI6MTYyNzU2NjAzMSwiaXNzIjoiYmxpaW5rIiwiZGF0YSI6eyJ0eXBlIjoiYWQtc2VydmVyIiwidHJhbnNhY3Rpb25JZCI6IjM1YmU1NDNjLTNkZTQtNGQ1Yy04N2NjLWIzYzEyOGZiYzU0MCIsIm5ldHdvcmtJZCI6MjEsInNpdGVJZCI6NTksInRhZ0lkIjo1OSwiY29va2llSWQiOiJjNGU4MWVhOS1jMjhmLTQwZDItODY1ZC1hNjQzZjE1OTcyZjUiLCJldmVudElkIjozLCJ0YXJnZXRpbmciOnsicGxhdGZvcm0iOiJXZWJzaXRlIiwiaXAiOiI3OC4xMjIuNzUuNzIiLCJ0aW1lIjoxNjI3NTY2MDMxLCJsb2NhdGlvbiI6eyJsYXRpdHVkZSI6NDguOTczOSwibG9uZ2l0dWRlIjozLjMxMTMsInJlZ2lvbiI6IkhERiIsImNvdW50cnkiOiJGUiIsImNpdHkiOiJTYXVsY2hlcnkiLCJ6aXBDb2RlIjoiMDIzMTAiLCJkZXBhcnRtZW50IjoiMDIifSwiY2l0eSI6IlNhdWxjaGVyeSIsImNvdW50cnkiOiJGUiIsImRldmljZU9zIjoibWFjT1MiLCJkZXZpY2VQbGF0Zm9ybSI6IldlYnNpdGUiLCJyYXdVc2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoTWFjaW50b3NoOyBJbnRlbCBNYWMgT1MgWCAxMF8xNV83KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvOTEuMC40NDcyLjEyNCBTYWZhcmkvNTM3LjM2In0sImdkcHIiOnsiaGFzQ29uc2VudCI6dHJ1ZX0sIndpbiI6ZmFsc2UsImFkSWQiOjU2NDgsImFkdmVydGlzZXJJZCI6MSwiY2FtcGFpZ25JZCI6MSwiY3JlYXRpdmVJZCI6MjgyNSwiZXJyb3IiOmZhbHNlfX0.-UefQH4G0k-RJGemBYffs-KL7EEwma2Wuwgk2xnpij8', }, headers: {}, - } -} + }; +}; /** * @description Mockup response from API for RTB creative @@ -213,8 +229,8 @@ const getConfigInterpretResponseRTB = (noAd = false, isInvalidVast = false) => { if (noAd) { return { message: 'invalid tag', - mode: 'no-ad' - } + mode: 'no-ad', + }; } const validVast = ` @@ -229,41 +245,43 @@ const getConfigInterpretResponseRTB = (noAd = false, isInvalidVast = false) => { - ` + `; const invalidVast = ` - ` + `; return { - body: { bids: [ - { - 'creative': { - 'video': { - 'content': isInvalidVast ? invalidVast : validVast, - 'height': 250, - 'width': 300 + body: { + bids: [ + { + creative: { + video: { + content: isInvalidVast ? invalidVast : validVast, + height: 250, + width: 300, + }, + media_type: 'video', + creativeId: 0, }, - 'media_type': 'video', - 'creativeId': 0, - }, - 'price': 0, - 'id': '8121', - 'token': 'token', - 'mode': 'rtb', - 'extras': { - 'deal_id': '34567ertyaza', - 'transaction_id': '2def0c5b2a7f6e' + price: 0, + id: '8121', + token: 'token', + mode: 'rtb', + extras: { + deal_id: '34567ertyaza', + transaction_id: '2def0c5b2a7f6e', + }, + currency: 'EUR', }, - 'currency': 'EUR' - } - ], - userSyncs: []} - } -} + ], + userSyncs: [], + }, + }; +}; /** * @@ -279,9 +297,9 @@ const testsGetMetaList = [ { title: 'Should return empty array if there are no parameters', args: { - fn: getMetaList() + fn: getMetaList(), }, - want: [] + want: [], }, { title: 'Should return list of metas with name associated', @@ -313,18 +331,63 @@ const testsGetMetaList = [ key: 'property', value: `'article:${'test'}'`, }, - ] - } -] + ], + }, +]; -describe('BLIINK Adapter getMetaList', function() { +describe('BLIINK Adapter getMetaList', function () { for (const test of testsGetMetaList) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); +const GetUserIds = [ + { + title: 'Should return undefined if there are no parameters', + args: { + fn: getUserIds(), + }, + want: undefined, + }, + { + title: 'Should return eids if exists', + args: { + fn: getUserIds([{ userIdAsEids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'testId', + 'atype': 1 + } + ] + } + ] }]), + }, + want: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'testId', + 'atype': 1 + } + ] + } + ], + }, +]; + +describe('BLIINK Adapter getUserIds', function () { + for (const test of GetUserIds) { + it(test.title, () => { + const res = test.args.fn; + expect(res).to.eql(test.want); + }); + } +}); /** * @description Array of tests used in describe function below @@ -349,127 +412,142 @@ const testsIsBidRequestValid = [ { title: 'isBidRequestValid format not valid', args: { - fn: spec.isBidRequestValid({}) + fn: spec.isBidRequestValid({}), }, want: false, }, { title: 'isBidRequestValid does not receive any bid', args: { - fn: spec.isBidRequestValid() + fn: spec.isBidRequestValid(), }, want: false, }, { title: 'isBidRequestValid Receive a valid bid', args: { - fn: spec.isBidRequestValid(getConfigBid('banner')) + fn: spec.isBidRequestValid(getConfigBid('banner')), }, want: true, - } -] + }, +]; -describe('BLIINK Adapter isBidRequestValid', function() { +describe('BLIINK Adapter isBidRequestValid', function () { for (const test of testsIsBidRequestValid) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); -const vastXml = getConfigInterpretResponseRTB().body.bids[0].creative.video.content +const vastXml = + getConfigInterpretResponseRTB().body.bids[0].creative.video.content; const testsInterpretResponse = [ { title: 'Should construct bid for video instream', args: { - fn: spec.interpretResponse(getConfigInterpretResponseRTB(false)) + fn: spec.interpretResponse(getConfigInterpretResponseRTB(false)), }, - want: [{ - cpm: 0, - currency: 'EUR', - height: 250, - width: 300, - creativeId: '34567ertyaza', - mediaType: 'video', - netRevenue: true, - requestId: '2def0c5b2a7f6e', - ttl: 300, - vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(vastXml.replace(/\\"/g, '"')) - }] + want: [ + { + cpm: 0, + currency: 'EUR', + height: 250, + width: 300, + creativeId: '34567ertyaza', + mediaType: 'video', + netRevenue: true, + requestId: '2def0c5b2a7f6e', + ttl: 300, + vastXml, + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(vastXml.replace(/\\"/g, '"')), + }, + ], }, { title: 'ServerResponse with message: invalid tag, return empty array', args: { - fn: spec.interpretResponse(getConfigInterpretResponse(true)) + fn: spec.interpretResponse(getConfigInterpretResponse(true)), }, - want: [] + want: [], }, { title: 'ServerResponse with mediaType banner', args: { - fn: spec.interpretResponse({body: {bids: [getConfigBannerBid()]}}), + fn: spec.interpretResponse({ body: { bids: [getConfigBannerBid()] } }), }, - want: [{ - ad: '', - cpm: 1, - creativeId: '34567erty', - currency: 'EUR', - height: 250, - mediaType: 'banner', - netRevenue: true, - requestId: '2def0c5b2a7f6e', - ttl: 300, - width: 300 - }] + want: [ + { + ad: '', + cpm: 1, + creativeId: '34567erty', + currency: 'EUR', + height: 250, + mediaType: 'banner', + netRevenue: true, + requestId: '2def0c5b2a7f6e', + ttl: 300, + width: 300, + }, + ], }, { title: 'ServerResponse with unhandled mediaType, return empty array', args: { - fn: spec.interpretResponse({body: {bids: [{...getConfigBannerBid(), - creative: { - unknown: { - adm: '', - height: 250, - width: 300, - }, - media_type: 'unknown', - creativeId: 125, - requestId: '2def0c5b2a7f6e', - }}]}}), + fn: spec.interpretResponse({ + body: { + bids: [ + { + ...getConfigBannerBid(), + creative: { + unknown: { + adm: '', + height: 250, + width: 300, + }, + media_type: 'unknown', + creativeId: 125, + requestId: '2def0c5b2a7f6e', + }, + }, + ], + }, + }), }, - want: [] + want: [], }, -] +]; -describe('BLIINK Adapter interpretResponse', function() { +describe('BLIINK Adapter interpretResponse', function () { for (const test of testsInterpretResponse) { it(test.title, () => { - const res = test.args.fn + const res = test.args.fn; if (res) { - expect(res).to.eql(test.want) + expect(res).to.eql(test.want); } - }) + }); } -}) +}); /** * @description Array of tests used in describe function below * @type {[ * {args: * {fn: { - * cpm: number, - * netRevenue: boolean, - * ad, requestId, - * meta: {mediaType}, - * width: number, - * currency: string, - * ttl: number, - * creativeId: number, - * height: number + * cpm: number, + * netRevenue: boolean, + * ad, requestId, + * meta: {mediaType}, + * width: number, + * currency: string, + * ttl: number, + * creativeId: number, + * height: number * } * }, want, title: string}]} */ @@ -478,21 +556,26 @@ const testsBuildBid = [ { title: 'Should return null if no bid passed in parameters', args: { - fn: buildBid() + fn: buildBid(), }, - want: null + want: null, }, { title: 'Input data must respect the output model', args: { - fn: buildBid({ id: 1, test: '123' }, { id: 2, test: '345' }, false, false) + fn: buildBid( + { id: 1, test: '123' }, + { id: 2, test: '345' }, + false, + false + ), }, - want: null + want: null, }, { title: 'input data respect the output model for video', args: { - fn: buildBid(getConfigVideoBid('video'), getConfigCreativeVideo()) + fn: buildBid(getConfigVideoBid('video'), getConfigCreativeVideo()), }, want: { requestId: getConfigBid('video').bidId, @@ -504,25 +587,31 @@ const testsBuildBid = [ creativeId: getConfigVideoBid().extras.deal_id, netRevenue: true, vastXml: getConfigCreativeVideo().vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), ttl: 300, - } + }, }, { title: 'use default height width output model for video', args: { - fn: buildBid({...getConfigVideoBid('video'), - creative: { - video: { - content: - '', - height: null, - width: null, + fn: buildBid( + { + ...getConfigVideoBid('video'), + creative: { + video: { + content: '', + height: null, + width: null, + }, + media_type: 'video', + creativeId: getConfigVideoBid().extras.deal_id, + requestId: '2def0c5b2a7f6e', }, - media_type: 'video', - creativeId: getConfigVideoBid().extras.deal_id, - requestId: '2def0c5b2a7f6e', - }}, getConfigCreativeVideo()) + }, + getConfigCreativeVideo() + ), }, want: { requestId: getConfigBid('video').bidId, @@ -534,14 +623,16 @@ const testsBuildBid = [ creativeId: getConfigVideoBid().extras.deal_id, netRevenue: true, vastXml: getConfigCreativeVideo().vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), ttl: 300, - } + }, }, { title: 'input data respect the output model for banner', args: { - fn: buildBid(getConfigBannerBid()) + fn: buildBid(getConfigBannerBid()), }, want: { requestId: getConfigBid('banner').bidId, @@ -554,18 +645,18 @@ const testsBuildBid = [ ad: getConfigBannerBid().creative.banner.adm, ttl: 300, netRevenue: true, - } - } -] + }, + }, +]; -describe('BLIINK Adapter buildBid', function() { +describe('BLIINK Adapter buildBid', function () { for (const test of testsBuildBid) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); /** * @description Array of tests used in describe function below @@ -575,28 +666,33 @@ const testsBuildRequests = [ { title: 'Should not build request, no bidder request exist', args: { - fn: spec.buildRequests() + fn: spec.buildRequests(), }, - want: null + want: null, }, { title: 'Should build request if bidderRequest exist', args: { - fn: spec.buildRequests([], getConfigBuildRequest('banner')) + fn: spec.buildRequests([], getConfigBuildRequest('banner')), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, keywords: '', pageDescription: '', pageTitle: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -605,35 +701,43 @@ const testsBuildRequests = [ }, ], }, - ] - } - } + ], + }, + }, }, { title: 'Should build request width GDPR configuration', args: { - fn: spec.buildRequests([], Object.assign(getConfigBuildRequest('banner'), { - gdprConsent: { - gdprApplies: true, - consentString: 'XXXX' - }, - })) + fn: spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, gdpr: true, gdprConsent: 'XXXX', pageDescription: '', pageTitle: '', keywords: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -642,52 +746,115 @@ const testsBuildRequests = [ }, ], }, - ] - } - } + ], + }, + }, + }, + { + title: 'Should build request width uspConsent if exists', + args: { + fn: spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + uspConsent: 'uspConsent', + }) + ), + }, + want: { + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + uspConsent: 'uspConsent', + gdprConsent: 'XXXX', + pageDescription: '', + pageTitle: '', + keywords: '', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + tags: [ + { + transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }, }, { title: 'Should build request width schain if exists', args: { - fn: spec.buildRequests([{schain: { - ver: '1.0', - complete: 1, - nodes: [{ - asi: 'ssp.test', - sid: '00001', - hp: 1 - }] - }}], Object.assign(getConfigBuildRequest('banner'), { - gdprConsent: { - gdprApplies: true, - consentString: 'XXXX' - }, - })) + fn: spec.buildRequests( + [ + { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'ssp.test', + sid: '00001', + hp: 1, + }, + ], + }, + }, + ], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, gdpr: true, gdprConsent: 'XXXX', pageDescription: '', pageTitle: '', keywords: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', schain: { ver: '1.0', complete: 1, - nodes: [{ - asi: 'ssp.test', - sid: '00001', - hp: 1 - }] + nodes: [ + { + asi: 'ssp.test', + sid: '00001', + hp: 1, + }, + ], }, tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -696,107 +863,313 @@ const testsBuildRequests = [ }, ], }, - ] - } - } - } -] + ], + }, + }, + }, + { + title: 'Should build request with eids if exists', + args: { + fn: spec.buildRequests( + [ + { + userIdAsEids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'vG4RRF93V05LRlJUTVVOQTJJJTJGbG1rZWxEeDVvc0NXWE42TzJqU2hG', + 'atype': 1 + } + ] + }, + { + 'source': 'netid.de', + 'uids': [ + { + 'id': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + 'atype': 1 + } + ] + } + ], + }, + ], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), + }, + want: { + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + gdprConsent: 'XXXX', + pageDescription: '', + pageTitle: '', + keywords: '', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + eids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'vG4RRF93V05LRlJUTVVOQTJJJTJGbG1rZWxEeDVvc0NXWE42TzJqU2hG', + 'atype': 1 + } + ] + }, + { + 'source': 'netid.de', + 'uids': [ + { + 'id': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + 'atype': 1 + } + ] + } + ], + tags: [ + { + transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }, + }, +]; -describe('BLIINK Adapter buildRequests', function() { +describe('BLIINK Adapter buildRequests', function () { for (const test of testsBuildRequests) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + test.args.after; + }); } -}) +}); const getSyncOptions = (pixelEnabled = true, iframeEnabled = false) => { return { pixelEnabled, - iframeEnabled - } -} + iframeEnabled, + }; +}; const getServerResponses = () => { return [ { - body: {bids: [], - userSyncs: [ { - type: 'script', - url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]' - }, - { - type: 'image', - url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D' - }]}, - } - ] -} + body: { + bids: [], + userSyncs: [ + { + type: 'script', + url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]', + }, + { + type: 'image', + url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D', + }, + ], + }, + }, + ]; +}; const getGdprConsent = () => { return { gdprApplies: 1, consentString: 'XXX', - apiVersion: 2 - } -} + apiVersion: 2, + }; +}; const testsGetUserSyncs = [ { title: 'Should not have gdprConsent exist', args: { - fn: spec.getUserSyncs(getSyncOptions(), getServerResponses(), getGdprConsent()) + fn: spec.getUserSyncs( + getSyncOptions(), + getServerResponses(), + getGdprConsent() + ), }, want: [ { type: 'script', - url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]' + url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]', }, { type: 'image', - url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D' - } - ] + url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D', + }, + ], }, { title: 'Should return iframe cookie sync if iframeEnabled', args: { - fn: spec.getUserSyncs(getSyncOptions(true, true), getServerResponses(), getGdprConsent()) + fn: spec.getUserSyncs( + getSyncOptions(true, true), + getServerResponses(), + getGdprConsent() + ), }, want: [ { type: 'iframe', - url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${getGdprConsent().gdprApplies}&coppa=0&gdprConsent=${getGdprConsent().consentString}&apiVersion=${getGdprConsent().apiVersion}` + url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${ + getGdprConsent().gdprApplies + }&coppa=0&gdprConsent=${getGdprConsent().consentString}&apiVersion=${ + getGdprConsent().apiVersion + }`, }, - ] + ], }, { title: 'ccpa', args: { - fn: spec.getUserSyncs(getSyncOptions(true, true), getServerResponses(), getGdprConsent(), 'ccpa-consent') + fn: spec.getUserSyncs( + getSyncOptions(true, true), + getServerResponses(), + getGdprConsent(), + 'ccpa-consent' + ), }, want: [ { type: 'iframe', - url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${getGdprConsent().gdprApplies}&coppa=0&uspConsent=ccpa-consent&gdprConsent=${getGdprConsent().consentString}&apiVersion=${getGdprConsent().apiVersion}` + url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${ + getGdprConsent().gdprApplies + }&coppa=0&uspConsent=ccpa-consent&gdprConsent=${ + getGdprConsent().consentString + }&apiVersion=${getGdprConsent().apiVersion}`, }, - ] + ], }, { title: 'Should output sync if no gdprConsent', args: { - fn: spec.getUserSyncs(getSyncOptions(), getServerResponses()) + fn: spec.getUserSyncs(getSyncOptions(), getServerResponses()), }, - want: getServerResponses()[0].body.userSyncs - } -] + want: getServerResponses()[0].body.userSyncs, + }, + { + title: 'Should output empty array if no pixelEnabled', + args: { + fn: spec.getUserSyncs({}, getServerResponses()), + }, + want: [], + }, +]; -describe('BLIINK Adapter getUserSyncs', function() { +describe('BLIINK Adapter getUserSyncs', function () { for (const test of testsGetUserSyncs) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); + +describe('BLIINK Adapter keywords & coppa true', function () { + it('Should build request with keyword and coppa true if exist', () => { + const metaElement = document.createElement('meta'); + metaElement.name = 'keywords'; + metaElement.content = 'Bliink, Saber, Prebid'; + sinon.stub(config, 'getConfig').withArgs('coppa').returns(true); + + const querySelectorStub = sinon + .stub(document, 'querySelector') + .returns(metaElement); + expect( + spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ) + ).to.eql({ + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + coppa: 1, + gdprConsent: 'XXXX', + pageDescription: 'Bliink, Saber, Prebid', + pageTitle: '', + keywords: 'Bliink,Saber,Prebid', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + tags: [ + { + transactionId: '2def0c5b2a7f6e', + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }); + querySelectorStub.restore(); + config.getConfig.restore(); + }); +}); + +describe('getEffectiveConnectionType', () => { + let navigatorStub; + + beforeEach(() => { + if ('connection' in navigator) { + navigatorStub = sinon.stub(navigator, 'connection').value({ + effectiveType: undefined, + }); + } + }); + + afterEach(() => { + if (navigatorStub) { + navigatorStub.restore(); + } + }); + if (navigatorStub) { + it('should return "unsupported" when effective connection type is undefined', () => { + const result = getEffectiveConnectionType(); + expect(result).to.equal('unsupported'); + }); + } +}); + +it('should expose gvlid', function () { + expect(spec.gvlid).to.equal(GVL_ID); +}); diff --git a/test/spec/modules/boldwinBidAdapter_spec.js b/test/spec/modules/boldwinBidAdapter_spec.js index 5b51183ea6d..52a6ec03757 100644 --- a/test/spec/modules/boldwinBidAdapter_spec.js +++ b/test/spec/modules/boldwinBidAdapter_spec.js @@ -19,7 +19,8 @@ describe('BoldwinBidAdapter', function () { const bidderRequest = { refererInfo: { referer: 'test.com' - } + }, + ortb2: {} }; describe('isBidRequestValid', function () { @@ -110,6 +111,36 @@ describe('BoldwinBidAdapter', function () { expect(data.placements).to.be.an('array').that.is.empty; }); }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { diff --git a/test/spec/modules/brandmetricsRtdProvider_spec.js b/test/spec/modules/brandmetricsRtdProvider_spec.js index 879ec7e1c7a..72a2e4b029c 100644 --- a/test/spec/modules/brandmetricsRtdProvider_spec.js +++ b/test/spec/modules/brandmetricsRtdProvider_spec.js @@ -1,5 +1,7 @@ import * as brandmetricsRTD from '../../../modules/brandmetricsRtdProvider.js'; import {config} from 'src/config.js'; +import * as events from '../../../src/events'; +import * as sinon from 'sinon'; const VALID_CONFIG = { name: 'brandmetrics', @@ -65,6 +67,8 @@ const NO_USP_CONSENT = { usp: '1NYY' }; +const UNDEFINED_USER_CONSENT = {}; + function mockSurveyLoaded(surveyConf) { const commands = window._brandmetrics || []; commands.forEach(command => { @@ -77,14 +81,16 @@ function mockSurveyLoaded(surveyConf) { }); } -function scriptTagExists(url) { - const tags = document.getElementsByTagName('script'); - for (let i = 0; i < tags.length; i++) { - if (tags[i].src === url) { - return true; +function mockCreativeInView(creativeInViewConf) { + const commands = window._brandmetrics || []; + commands.forEach(command => { + if (command.cmd === '_addeventlistener') { + const conf = command.val; + if (conf.event === 'creative_in_view') { + conf.handler(creativeInViewConf); + } } - } - return false; + }) } describe('BrandmetricsRTD module', () => { @@ -116,6 +122,10 @@ describe('BrandmetricsRTD module', () => { it('should not init when there is no usp- consent', () => { expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_USP_CONSENT)).to.equal(false); }); + + it('should init if there are no consent- objects defined', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, UNDEFINED_USER_CONSENT)).to.equal(true); + }); }); describe('getBidRequestData', () => { @@ -188,4 +198,62 @@ describe('getBidRequestData', () => { expect(bidderOrtb2[exp].user.ext.data.brandmetrics_survey).to.equal('mockMeasurementId') }) }); + + describe('billable events', () => { + let sandbox; + let eventsEmitSpy; + + before(() => { + sandbox = sinon.sandbox.create(); + eventsEmitSpy = sandbox.spy(events, ['emit']); + }); + + beforeEach(() => { + eventsEmitSpy.resetHistory(); + }) + + afterEach(() => { + sandbox.restore(); + }); + + it('should emit billable event from prebid events', () => { + const expectedEvent = { + vendor: 'brandmetrics', + type: 'creative_in_view', + measurementId: 'mockMeasurementId', + auctionId: 'mockAuctionId', + transactionId: 'mockTransactionId' + }; + + mockCreativeInView({ + mid: expectedEvent.measurementId, + source: { + type: 'pbj', + data: { + auctionId: expectedEvent.auctionId, + transactionId: expectedEvent.transactionId + }, + } + }); + + expect(eventsEmitSpy.callCount).to.equal(1); + + const event = eventsEmitSpy.getCalls()[0].args[1]; + delete event['billingId']; + + expect(event).to.deep.equal(expectedEvent); + }); + + it('should not emit billable event from non prebid- sources', () => { + mockCreativeInView({ + mid: 'mockMeasurementId', + source: { + type: 'gpt', + data: {}, + } + }); + + expect(eventsEmitSpy.callCount).to.equal(0); + }); + }); }); diff --git a/test/spec/modules/bridBidAdapter_spec.js b/test/spec/modules/bridBidAdapter_spec.js new file mode 100644 index 00000000000..7503c748999 --- /dev/null +++ b/test/spec/modules/bridBidAdapter_spec.js @@ -0,0 +1,129 @@ +import { spec } from '../../../modules/bridBidAdapter.js' + +describe('Brid Bid Adapter', function() { + const videoRequest = [{ + bidder: 'brid', + params: { + placementId: 12345, + }, + mediaTypes: { + video: { + playerSize: [[640, 360]], + context: 'instream', + playbackmethod: [1, 2, 3, 4] + } + } + }]; + + it('Test the bid validation function', function() { + const validBid = spec.isBidRequestValid(videoRequest[0]); + const invalidBid = spec.isBidRequestValid(null); + + expect(validBid).to.be.true; + expect(invalidBid).to.be.false; + }); + + it('Test the request processing function', function () { + const request = spec.buildRequests(videoRequest, videoRequest[0]); + expect(request).to.not.be.empty; + + const payload = JSON.parse(request[0].data); + expect(payload).to.not.be.empty; + expect(payload.sdk).to.deep.equal({ + source: 'pbjs', + version: '$prebid.version$' + }); + expect(payload.imp[0].ext.prebid.storedrequest.id).to.equal(12345); + }); + + it('Test nobid responses', function () { + const responseBody = { + 'id': 'test-id', + 'cur': 'USD', + 'seatbid': [], + 'nbr': 0 + }; + const bidderRequest = null; + + const bidResponse = spec.interpretResponse({ body: responseBody }, {bidderRequest}); + + expect(bidResponse.length).to.equal(0); + }); + + it('Test the response parsing function', function () { + const responseBody = { + 'id': 'test-id', + 'cur': 'USD', + 'seatbid': [{ + 'bid': [{ + 'id': '5044997188309660254', + 'price': 5, + 'adm': 'test ad', + 'adid': '97517771', + 'crid': '97517771', + 'adomain': ['domain.com'], + 'w': 640, + 'h': 480 + }], + 'seat': 'bidder' + }] + }; + const bidderRequest = { + bidderCode: 'brid', + bidderRequestId: '22edbae2733bf6', + bids: videoRequest + }; + + const bidResponse = spec.interpretResponse({ body: responseBody }, {bidderRequest}); + expect(bidResponse).to.not.be.empty; + + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(5); + expect(bid.width).to.equal(640); + expect(bid.height).to.equal(480); + expect(bid.currency).to.equal('USD'); + }); + + it('Test GDPR and USP consents are present in the request', function () { + let gdprConsentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let uspConsentString = '1YA-'; + let bidderRequest = { + 'bidderCode': 'brid', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': uspConsentString, + 'gdprConsent': { + consentString: gdprConsentString, + gdprApplies: true, + addtlConsent: '1~7.12.35.62.66.70.89.93.108' + } + }; + bidderRequest.bids = videoRequest; + + const request = spec.buildRequests(videoRequest, bidderRequest); + const payload = JSON.parse(request[0].data); + + expect(payload.user.ext.consent).to.equal(gdprConsentString); + expect(payload.regs.ext.us_privacy).to.equal(uspConsentString); + expect(payload.regs.ext.gdpr).to.equal(1); + }); + + it('Test GDPR is not present', function () { + let uspConsentString = '1YA-'; + let bidderRequest = { + 'bidderCode': 'brid', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': uspConsentString + }; + bidderRequest.bids = videoRequest; + + const request = spec.buildRequests(videoRequest, bidderRequest); + const payload = JSON.parse(request[0].data); + + expect(payload.regs.ext.gdpr).to.be.undefined; + expect(payload.regs.ext.us_privacy).to.equal(uspConsentString); + }); +}); diff --git a/test/spec/modules/byDataAnalyticsAdapter_spec.js b/test/spec/modules/byDataAnalyticsAdapter_spec.js index 1346284695c..c680c687a71 100644 --- a/test/spec/modules/byDataAnalyticsAdapter_spec.js +++ b/test/spec/modules/byDataAnalyticsAdapter_spec.js @@ -96,7 +96,7 @@ let expectedDataArgs = { aus: '300x250', bidadv: 'appnexus', bid: '14480e9832f2d2b', - inb: 0, + inb: 1, ito: 0, ipwb: 0, iwb: 0, @@ -107,7 +107,7 @@ let expectedDataArgs = { aus: '250x250', bidadv: 'appnexus', bid: '14480e9832f2d2b', - inb: 0, + inb: 1, ito: 0, ipwb: 0, iwb: 0, @@ -167,11 +167,13 @@ describe('byData Analytics Adapter ', () => { }); describe('track-events', function () { - ascAdapter.enableAnalytics(initOptions) - // Step 1: Initialize adapter - adapterManager.enableAnalytics({ - provider: 'bydata', - options: initOptions + before(() => { + ascAdapter.enableAnalytics(initOptions) + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'bydata', + options: initOptions + }); }); it('sends and formatted auction data ', function () { events.emit(constants.EVENTS.BID_TIMEOUT, bidTimeoutArgs); diff --git a/test/spec/modules/cadentApertureMXBidAdapter_spec.js b/test/spec/modules/cadentApertureMXBidAdapter_spec.js index 091a8105354..3ccb5405552 100644 --- a/test/spec/modules/cadentApertureMXBidAdapter_spec.js +++ b/test/spec/modules/cadentApertureMXBidAdapter_spec.js @@ -1,7 +1,8 @@ -import { expect } from 'chai'; -import { spec } from 'modules/cadentApertureMXBidAdapter.js'; import * as utils from 'src/utils.js'; + +import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec } from 'modules/cadentApertureMXBidAdapter.js'; describe('cadent_aperture_mx Adapter', function () { describe('callBids', function () { @@ -236,228 +237,312 @@ describe('cadent_aperture_mx Adapter', function () { 'bidId': '30b31c2501de1e', 'auctionId': 'e19f1eff-8b27-42a6-888d-9674e5a6130c', 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'ortb2Imp': { + 'ext': { + 'tid': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ed', + }, + }, }] }; let request = spec.buildRequests(bidderRequest.bids, bidderRequest); - it('sends bid request to ENDPOINT via POST', function () { - expect(request.method).to.equal('POST'); - }); + describe('non-gpp tests', function() { + it('sends bid request to ENDPOINT via POST', function () { + expect(request.method).to.equal('POST'); + }); - it('contains the correct options', function () { - expect(request.options.withCredentials).to.equal(true); - }); + it('contains the correct options', function () { + expect(request.options.withCredentials).to.equal(true); + }); - it('contains a properly formatted endpoint url', function () { - const url = request.url.split('?'); - const queryParams = url[1].split('&'); - expect(queryParams[0]).to.match(new RegExp('^t=\d*', 'g')); - expect(queryParams[1]).to.match(new RegExp('^ts=\d*', 'g')); - }); + it('contains a properly formatted endpoint url', function () { + const url = request.url.split('?'); + const queryParams = url[1].split('&'); + expect(queryParams[0]).to.match(new RegExp('^t=\d*', 'g')); + expect(queryParams[1]).to.match(new RegExp('^ts=\d*', 'g')); + }); - it('builds bidfloor value from bid param when getFloor function does not exist', function () { - const bidRequestWithFloor = utils.deepClone(bidderRequest.bids); - bidRequestWithFloor[0].params.bidfloor = 1; - const requestWithFloor = spec.buildRequests(bidRequestWithFloor, bidderRequest); - const data = JSON.parse(requestWithFloor.data); - expect(data.imp[0].bidfloor).to.equal(bidRequestWithFloor[0].params.bidfloor); - }); + it('builds bidfloor value from bid param when getFloor function does not exist', function () { + const bidRequestWithFloor = utils.deepClone(bidderRequest.bids); + bidRequestWithFloor[0].params.bidfloor = 1; + const requestWithFloor = spec.buildRequests(bidRequestWithFloor, bidderRequest); + const data = JSON.parse(requestWithFloor.data); + expect(data.imp[0].bidfloor).to.equal(bidRequestWithFloor[0].params.bidfloor); + }); - it('builds bidfloor value from getFloor function when it exists', function () { - const floorResponse = { currency: 'USD', floor: 3 }; - const bidRequestWithGetFloor = utils.deepClone(bidderRequest.bids); - bidRequestWithGetFloor[0].getFloor = () => floorResponse; - const requestWithGetFloor = spec.buildRequests(bidRequestWithGetFloor, bidderRequest); - const data = JSON.parse(requestWithGetFloor.data); - expect(data.imp[0].bidfloor).to.equal(3); - }); + it('builds bidfloor value from getFloor function when it exists', function () { + const floorResponse = { currency: 'USD', floor: 3 }; + const bidRequestWithGetFloor = utils.deepClone(bidderRequest.bids); + bidRequestWithGetFloor[0].getFloor = () => floorResponse; + const requestWithGetFloor = spec.buildRequests(bidRequestWithGetFloor, bidderRequest); + const data = JSON.parse(requestWithGetFloor.data); + expect(data.imp[0].bidfloor).to.equal(3); + }); - it('builds bidfloor value from getFloor when both floor and getFloor function exists', function () { - const floorResponse = { currency: 'USD', floor: 3 }; - const bidRequestWithBothFloors = utils.deepClone(bidderRequest.bids); - bidRequestWithBothFloors[0].params.bidfloor = 1; - bidRequestWithBothFloors[0].getFloor = () => floorResponse; - const requestWithBothFloors = spec.buildRequests(bidRequestWithBothFloors, bidderRequest); - const data = JSON.parse(requestWithBothFloors.data); - expect(data.imp[0].bidfloor).to.equal(3); - }); + it('builds bidfloor value from getFloor when both floor and getFloor function exists', function () { + const floorResponse = { currency: 'USD', floor: 3 }; + const bidRequestWithBothFloors = utils.deepClone(bidderRequest.bids); + bidRequestWithBothFloors[0].params.bidfloor = 1; + bidRequestWithBothFloors[0].getFloor = () => floorResponse; + const requestWithBothFloors = spec.buildRequests(bidRequestWithBothFloors, bidderRequest); + const data = JSON.parse(requestWithBothFloors.data); + expect(data.imp[0].bidfloor).to.equal(3); + }); - it('empty bidfloor value when floor and getFloor is not defined', function () { - const bidRequestWithoutFloor = utils.deepClone(bidderRequest.bids); - const requestWithoutFloor = spec.buildRequests(bidRequestWithoutFloor, bidderRequest); - const data = JSON.parse(requestWithoutFloor.data); - expect(data.imp[0].bidfloor).to.not.exist; - }); + it('empty bidfloor value when floor and getFloor is not defined', function () { + const bidRequestWithoutFloor = utils.deepClone(bidderRequest.bids); + const requestWithoutFloor = spec.buildRequests(bidRequestWithoutFloor, bidderRequest); + const data = JSON.parse(requestWithoutFloor.data); + expect(data.imp[0].bidfloor).to.not.exist; + }); - it('builds request properly', function () { - const data = JSON.parse(request.data); - expect(Array.isArray(data.imp)).to.equal(true); - expect(data.id).to.equal(bidderRequest.auctionId); - expect(data.imp.length).to.equal(1); - expect(data.imp[0].id).to.equal('30b31c2501de1e'); - expect(data.imp[0].tid).to.equal('d7b773de-ceaa-484d-89ca-d9f51b8d61ec'); - expect(data.imp[0].tagid).to.equal('25251'); - expect(data.imp[0].secure).to.equal(0); - expect(data.imp[0].vastXml).to.equal(undefined); - }); + it('builds request properly', function () { + const data = JSON.parse(request.data); + expect(Array.isArray(data.imp)).to.equal(true); + expect(data.id).to.equal(bidderRequest.auctionId); + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal('30b31c2501de1e'); + expect(data.imp[0].tid).to.equal('d7b773de-ceaa-484d-89ca-d9f51b8d61ed'); + expect(data.imp[0].tagid).to.equal('25251'); + expect(data.imp[0].secure).to.equal(0); + expect(data.imp[0].vastXml).to.equal(undefined); + }); - it('properly sends site information and protocol', function () { - request = spec.buildRequests(bidderRequest.bids, bidderRequest); - request = JSON.parse(request.data); - expect(request.site).to.have.property('domain', 'example.com'); - expect(request.site).to.have.property('page', 'https://example.com/index.html?pbjs_debug=true'); - expect(request.site).to.have.property('ref', 'https://referrer.com'); - }); + it('populates id even when auctionId is not available', function () { + // addressing https://github.com/prebid/Prebid.js/issues/9781 + bidderRequest.auctionId = null; + request = spec.buildRequests(bidderRequest.bids, bidderRequest); - it('builds correctly formatted request banner object', function () { - let bidRequestWithBanner = utils.deepClone(bidderRequest.bids); - let request = spec.buildRequests(bidRequestWithBanner, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.equal(undefined); - expect(data.imp[0].banner).to.exist.and.to.be.a('object'); - expect(data.imp[0].banner.w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); - expect(data.imp[0].banner.h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); - expect(data.imp[0].banner.format[0].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); - expect(data.imp[0].banner.format[0].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); - expect(data.imp[0].banner.format[1].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][0]); - expect(data.imp[0].banner.format[1].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][1]); - }); + const data = JSON.parse(request.data); + expect(data.id).not.to.be.null; + expect(data.id).not.to.equal(bidderRequest.auctionId); + }); - it('builds correctly formatted request video object for instream', function () { - let bidRequestWithVideo = utils.deepClone(bidderRequest.bids); - bidRequestWithVideo[0].mediaTypes = { - video: { - context: 'instream', - playerSize: [[640, 480]] - }, - }; - bidRequestWithVideo[0].params.video = {}; - let request = spec.buildRequests(bidRequestWithVideo, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.exist.and.to.be.a('object'); - expect(data.imp[0].video.w).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][0]); - expect(data.imp[0].video.h).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][1]); - }); + it('properly sends site information and protocol', function () { + request = spec.buildRequests(bidderRequest.bids, bidderRequest); + request = JSON.parse(request.data); + expect(request.site).to.have.property('domain', 'example.com'); + expect(request.site).to.have.property('page', 'https://example.com/index.html?pbjs_debug=true'); + expect(request.site).to.have.property('ref', 'https://referrer.com'); + }); - it('builds correctly formatted request video object for outstream', function () { - let bidRequestWithOutstreamVideo = utils.deepClone(bidderRequest.bids); - bidRequestWithOutstreamVideo[0].mediaTypes = { - video: { - context: 'outstream', - playerSize: [[640, 480]] - }, - }; - bidRequestWithOutstreamVideo[0].params.video = {}; - let request = spec.buildRequests(bidRequestWithOutstreamVideo, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.exist.and.to.be.a('object'); - expect(data.imp[0].video.w).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][0]); - expect(data.imp[0].video.h).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][1]); - }); + it('builds correctly formatted request banner object', function () { + let bidRequestWithBanner = utils.deepClone(bidderRequest.bids); + let request = spec.buildRequests(bidRequestWithBanner, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.equal(undefined); + expect(data.imp[0].banner).to.exist.and.to.be.a('object'); + expect(data.imp[0].banner.w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); + expect(data.imp[0].banner.h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); + expect(data.imp[0].banner.format[0].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); + expect(data.imp[0].banner.format[0].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); + expect(data.imp[0].banner.format[1].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][0]); + expect(data.imp[0].banner.format[1].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][1]); + }); - it('shouldn\'t contain a user obj without GDPR information', function () { - let request = spec.buildRequests(bidderRequest.bids, bidderRequest) - request = JSON.parse(request.data) - expect(request).to.not.have.property('user'); - }); + it('builds correctly formatted request video object for instream', function () { + let bidRequestWithVideo = utils.deepClone(bidderRequest.bids); + bidRequestWithVideo[0].mediaTypes = { + video: { + context: 'instream', + playerSize: [[640, 480]] + }, + }; + bidRequestWithVideo[0].params.video = {}; + let request = spec.buildRequests(bidRequestWithVideo, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist.and.to.be.a('object'); + expect(data.imp[0].video.w).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][0]); + expect(data.imp[0].video.h).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][1]); + }); - it('should have the right gdpr info when enabled', function () { - let consentString = 'OIJSZsOAFsABAB8EMXZZZZZ+A=='; - const gdprBidderRequest = utils.deepClone(bidderRequest); - gdprBidderRequest.gdprConsent = { - 'consentString': consentString, - 'gdprApplies': true - }; - let request = spec.buildRequests(gdprBidderRequest.bids, gdprBidderRequest); + it('builds correctly formatted request video object for outstream', function () { + let bidRequestWithOutstreamVideo = utils.deepClone(bidderRequest.bids); + bidRequestWithOutstreamVideo[0].mediaTypes = { + video: { + context: 'outstream', + playerSize: [[640, 480]] + }, + }; + bidRequestWithOutstreamVideo[0].params.video = {}; + let request = spec.buildRequests(bidRequestWithOutstreamVideo, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist.and.to.be.a('object'); + expect(data.imp[0].video.w).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][0]); + expect(data.imp[0].video.h).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][1]); + }); - request = JSON.parse(request.data) - expect(request.regs.ext).to.have.property('gdpr', 1); - expect(request.user.ext).to.have.property('consent', consentString); - }); + it('shouldn\'t contain a user obj without GDPR information', function () { + let request = spec.buildRequests(bidderRequest.bids, bidderRequest) + request = JSON.parse(request.data) + expect(request).to.not.have.property('user'); + }); - it('should\'t contain consent string if gdpr isn\'t applied', function () { - const nonGdprBidderRequest = utils.deepClone(bidderRequest); - nonGdprBidderRequest.gdprConsent = { - 'gdprApplies': false - }; - let request = spec.buildRequests(nonGdprBidderRequest.bids, nonGdprBidderRequest); - request = JSON.parse(request.data) - expect(request.regs.ext).to.have.property('gdpr', 0); - expect(request).to.not.have.property('user'); - }); + it('should have the right gdpr info when enabled', function () { + let consentString = 'OIJSZsOAFsABAB8EMXZZZZZ+A=='; + const gdprBidderRequest = utils.deepClone(bidderRequest); + gdprBidderRequest.gdprConsent = { + 'consentString': consentString, + 'gdprApplies': true + }; + let request = spec.buildRequests(gdprBidderRequest.bids, gdprBidderRequest); + + request = JSON.parse(request.data) + expect(request.regs.ext).to.have.property('gdpr', 1); + expect(request.user.ext).to.have.property('consent', consentString); + }); - it('should add us privacy info to request', function() { - const uspBidderRequest = utils.deepClone(bidderRequest); - let consentString = '1YNN'; - uspBidderRequest.uspConsent = consentString; - let request = spec.buildRequests(uspBidderRequest.bids, uspBidderRequest); - request = JSON.parse(request.data); - expect(request.us_privacy).to.exist; - expect(request.us_privacy).to.exist.and.to.equal(consentString); - }); + it('should\'t contain consent string if gdpr isn\'t applied', function () { + const nonGdprBidderRequest = utils.deepClone(bidderRequest); + nonGdprBidderRequest.gdprConsent = { + 'gdprApplies': false + }; + let request = spec.buildRequests(nonGdprBidderRequest.bids, nonGdprBidderRequest); + request = JSON.parse(request.data) + expect(request.regs.ext).to.have.property('gdpr', 0); + expect(request).to.not.have.property('user'); + }); - it('should add schain object to request', function() { - const schainBidderRequest = utils.deepClone(bidderRequest); - schainBidderRequest.bids[0].schain = { - 'complete': 1, - 'ver': '1.0', - 'nodes': [ - { - 'asi': 'testing.com', - 'sid': 'abc', - 'hp': 1 - } - ] - }; - let request = spec.buildRequests(schainBidderRequest.bids, schainBidderRequest); - request = JSON.parse(request.data); - expect(request.source.ext.schain).to.exist; - expect(request.source.ext.schain).to.have.property('complete', 1); - expect(request.source.ext.schain).to.have.property('ver', '1.0'); - expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.bids[0].schain.nodes[0].asi); - }); + it('should add us privacy info to request', function() { + const uspBidderRequest = utils.deepClone(bidderRequest); + let consentString = '1YNN'; + uspBidderRequest.uspConsent = consentString; + let request = spec.buildRequests(uspBidderRequest.bids, uspBidderRequest); + request = JSON.parse(request.data); + expect(request.us_privacy).to.exist; + expect(request.us_privacy).to.exist.and.to.equal(consentString); + }); - it('should add liveramp identitylink id to request', () => { - const idl_env = '123'; - const bidRequestWithID = utils.deepClone(bidderRequest); - bidRequestWithID.userId = { idl_env }; - let requestWithID = spec.buildRequests(bidRequestWithID.bids, bidRequestWithID); - requestWithID = JSON.parse(requestWithID.data); - expect(requestWithID.user.ext.eids[0]).to.deep.equal({ - source: 'liveramp.com', - uids: [{ - id: idl_env, - ext: { - rtiPartner: 'idl' - } - }] + it('should add schain object to request', function() { + const schainBidderRequest = utils.deepClone(bidderRequest); + schainBidderRequest.bids[0].schain = { + 'complete': 1, + 'ver': '1.0', + 'nodes': [ + { + 'asi': 'testing.com', + 'sid': 'abc', + 'hp': 1 + } + ] + }; + let request = spec.buildRequests(schainBidderRequest.bids, schainBidderRequest); + request = JSON.parse(request.data); + expect(request.source.ext.schain).to.exist; + expect(request.source.ext.schain).to.have.property('complete', 1); + expect(request.source.ext.schain).to.have.property('ver', '1.0'); + expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.bids[0].schain.nodes[0].asi); + }); + + it('should add liveramp identitylink id to request', () => { + const idl_env = '123'; + const bidRequestWithID = utils.deepClone(bidderRequest); + bidRequestWithID.userId = { idl_env }; + let requestWithID = spec.buildRequests(bidRequestWithID.bids, bidRequestWithID); + requestWithID = JSON.parse(requestWithID.data); + expect(requestWithID.user.ext.eids[0]).to.deep.equal({ + source: 'liveramp.com', + uids: [{ + id: idl_env, + ext: { + rtiPartner: 'idl' + } + }] + }); + }); + + it('should add gpid to request if present', () => { + const gpid = '/12345/my-gpt-tag-0'; + let bid = utils.deepClone(bidderRequest.bids[0]); + bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; + bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; + let requestWithGPID = spec.buildRequests([bid], bidderRequest); + requestWithGPID = JSON.parse(requestWithGPID.data); + expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); }); - }); - it('should add gpid to request if present', () => { - const gpid = '/12345/my-gpt-tag-0'; - let bid = utils.deepClone(bidderRequest.bids[0]); - bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; - bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; - let requestWithGPID = spec.buildRequests([bid], bidderRequest); - requestWithGPID = JSON.parse(requestWithGPID.data); - expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); + it('should add UID 2.0 to request', () => { + const uid2 = { id: '456' }; + const bidRequestWithUID = utils.deepClone(bidderRequest); + bidRequestWithUID.userId = { uid2 }; + let requestWithUID = spec.buildRequests(bidRequestWithUID.bids, bidRequestWithUID); + requestWithUID = JSON.parse(requestWithUID.data); + expect(requestWithUID.user.ext.eids[0]).to.deep.equal({ + source: 'uidapi.com', + uids: [{ + id: uid2.id, + ext: { + rtiPartner: 'UID2' + } + }] + }); + }); }); - it('should add UID 2.0 to request', () => { - const uid2 = { id: '456' }; - const bidRequestWithUID = utils.deepClone(bidderRequest); - bidRequestWithUID.userId = { uid2 }; - let requestWithUID = spec.buildRequests(bidRequestWithUID.bids, bidRequestWithUID); - requestWithUID = JSON.parse(requestWithUID.data); - expect(requestWithUID.user.ext.eids[0]).to.deep.equal({ - source: 'uidapi.com', - uids: [{ - id: uid2.id, - ext: { - rtiPartner: 'UID2' - } - }] + describe('gpp tests', function() { + describe('when gppConsent is not present on bid request', () => { + it('should return request with no gpp or gpp_sid properties', function() { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp).to.be.undefined; + expect(request?.regs?.gpp_sid).to.be.undefined; + }); + }); + + describe('when gppConsent is present on bid request', () => { + describe('gppString', () => { + describe('is not defined on request', () => { + it('should return request with gpp undefined', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp).to.be.undefined; + }); + }); + + describe('is defined on request', () => { + it('should return request with gpp set correctly', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + const gppString = 'abcdefgh'; + gppCompliantBidderRequest.gppConsent = { + gppString + } + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request.regs.gpp).to.exist.and.to.equal(gppString); + }); + }); + }); + + describe('applicableSections', () => { + describe('is not defined on request', () => { + it('should return request with gpp_sid undefined', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp_sid).to.be.undefined; + }); + }); + + describe('is defined on request', () => { + it('should return request with gpp_sid set correctly', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + const applicableSections = [8]; + gppCompliantBidderRequest.gppConsent = { + applicableSections + } + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request.regs.gpp_sid).to.deep.equal(applicableSections); + }); + }); + }); }); }); }); @@ -699,7 +784,7 @@ describe('cadent_aperture_mx Adapter', function () { it('should not throw an error when decoding an improperly encoded adm', function () { const badAdmServerResponse = utils.deepClone(serverResponse); - badAdmServerResponse.seatbid[0].bid[0].adm = '\\<\\/script\\>'; + badAdmServerResponse.seatbid[0].bid[0].adm = '\\<\\/script\\>'; badAdmServerResponse.seatbid[1].bid[0].adm = '%3F%%3Dcadent%3C3prebid'; assert.doesNotThrow(() => spec.interpretResponse({ @@ -764,5 +849,38 @@ describe('cadent_aperture_mx Adapter', function () { expect(syncs[0].url).to.contains('usp=test'); expect(syncs[0].url).to.equal('https://biddr.brealtime.com/check.html?gdpr=1&gdpr_consent=test&usp=test') }); + + it('should pass gpp string and section id', function() { + let syncs = spec.getUserSyncs({iframeEnabled: true}, {}, {}, {}, { + gppString: 'abcdefgs', + applicableSections: [1, 2, 4] + }); + expect(syncs).to.not.be.an('undefined'); + expect(syncs[0].url).to.contains('gpp=abcdefgs') + expect(syncs[0].url).to.contains('gpp_sid=1,2,4') + }); + + it('should pass us_privacy and gdpr string and gpp string', function () { + let syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, + { + gdprApplies: true, + consentString: 'test' + }, + { + consentString: 'test' + }, + { + gppString: 'abcdefgs', + applicableSections: [1, 2, 4] + } + ); + expect(syncs).to.not.be.an('undefined'); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.contains('gdpr=1'); + expect(syncs[0].url).to.contains('usp=test'); + expect(syncs[0].url).to.contains('gpp=abcdefgs'); + expect(syncs[0].url).to.equal('https://biddr.brealtime.com/check.html?gdpr=1&gdpr_consent=test&usp=test&gpp=abcdefgs&gpp_sid=1,2,4'); + }); }); }); diff --git a/test/spec/modules/categoryTranslation_spec.js b/test/spec/modules/categoryTranslation_spec.js index 2301d6aab1b..d4f6aa66c7d 100644 --- a/test/spec/modules/categoryTranslation_spec.js +++ b/test/spec/modules/categoryTranslation_spec.js @@ -2,18 +2,16 @@ import { getAdserverCategoryHook, initTranslation, storage } from 'modules/categ import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; import { expect } from 'chai'; +import {server} from '../../mocks/xhr.js'; describe('category translation', function () { - let fakeTranslationServer; let getLocalStorageStub; beforeEach(function () { - fakeTranslationServer = sinon.fakeServer.create(); getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); afterEach(function() { - fakeTranslationServer.reset(); getLocalStorageStub.restore(); config.resetConfig(); }); @@ -73,7 +71,7 @@ describe('category translation', function () { } })); initTranslation(); - expect(fakeTranslationServer.requests.length).to.equal(0); + expect(server.requests.length).to.equal(0); clock.restore(); }); @@ -86,15 +84,15 @@ describe('category translation', function () { } })); initTranslation(); - expect(fakeTranslationServer.requests.length).to.equal(1); + expect(server.requests.length).to.equal(1); clock.restore(); }); it('should use default mapping file if publisher has not defined in config', function () { getLocalStorageStub.returns(null); initTranslation('http://sample.com', 'somekey'); - expect(fakeTranslationServer.requests.length).to.equal(1); - expect(fakeTranslationServer.requests[0].url).to.equal('http://sample.com'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://sample.com/'); }); it('should use publisher defined mapping file', function () { @@ -105,7 +103,7 @@ describe('category translation', function () { }); getLocalStorageStub.returns(null); initTranslation('http://sample.com', 'somekey'); - expect(fakeTranslationServer.requests.length).to.equal(2); - expect(fakeTranslationServer.requests[0].url).to.equal('http://sample.com'); + expect(server.requests.length).to.equal(2); + expect(server.requests[0].url).to.equal('http://sample.com/'); }); }); diff --git a/test/spec/modules/chtnwBidAdapter_spec.js b/test/spec/modules/chtnwBidAdapter_spec.js index 3875de6c4fa..0fdd1a3f19b 100644 --- a/test/spec/modules/chtnwBidAdapter_spec.js +++ b/test/spec/modules/chtnwBidAdapter_spec.js @@ -39,7 +39,10 @@ describe('ChtnwAdapter', function () { ], }]; - const request = spec.buildRequests(bidRequests); + let request; + before(() => { + request = spec.buildRequests(bidRequests); + }) it('Returns POST method', function () { expect(request.method).to.equal('POST'); diff --git a/test/spec/modules/concertBidAdapter_spec.js b/test/spec/modules/concertBidAdapter_spec.js index 96c98e5e5a2..0a76ed3e62d 100644 --- a/test/spec/modules/concertBidAdapter_spec.js +++ b/test/spec/modules/concertBidAdapter_spec.js @@ -94,7 +94,7 @@ describe('ConcertAdapter', function () { }); describe('spec.isBidRequestValid', function() { - it('should return when it recieved all the required params', function() { + it('should return when it received all the required params', function() { const bid = bidRequests[0]; expect(spec.isBidRequestValid(bid)).to.equal(true); }); @@ -116,7 +116,20 @@ describe('ConcertAdapter', function () { expect(payload).to.have.property('meta'); expect(payload).to.have.property('slots'); - const metaRequiredFields = ['prebidVersion', 'pageUrl', 'screen', 'debug', 'uid', 'optedOut', 'adapterVersion', 'uspConsent', 'gdprConsent', 'gppConsent']; + const metaRequiredFields = [ + 'prebidVersion', + 'pageUrl', + 'screen', + 'debug', + 'uid', + 'optedOut', + 'adapterVersion', + 'uspConsent', + 'gdprConsent', + 'gppConsent', + 'browserLanguage', + 'tdid' + ]; const slotsRequiredFields = ['name', 'bidId', 'transactionId', 'sizes', 'partnerId', 'slotType']; metaRequiredFields.forEach(function(field) { @@ -199,6 +212,31 @@ describe('ConcertAdapter', function () { expect(slot.offsetCoordinates.x).to.equal(100) expect(slot.offsetCoordinates.y).to.equal(0) }) + + it('should not pass along tdid if the user has opted out', function() { + storage.setDataInLocalStorage('c_nap', 'true'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.tdid).to.be.null; + }); + + it('should not pass along tdid if USP consent disallows', function() { + storage.removeDataFromLocalStorage('c_nap'); + const request = spec.buildRequests(bidRequests, { ...bidRequest, uspConsent: '1YY' }); + const payload = JSON.parse(request.data); + + expect(payload.meta.tdid).to.be.null; + }); + + it('should pass along tdid if the user has not opted out', function() { + storage.removeDataFromLocalStorage('c_nap', 'true'); + const tdid = '123abc'; + const bidRequestsWithTdid = [{ ...bidRequests[0], userId: { tdid } }] + const request = spec.buildRequests(bidRequestsWithTdid, bidRequest); + const payload = JSON.parse(request.data); + expect(payload.meta.tdid).to.equal(tdid); + }); }); describe('spec.interpretResponse', function() { diff --git a/test/spec/modules/connatixBidAdapter_spec.js b/test/spec/modules/connatixBidAdapter_spec.js new file mode 100644 index 00000000000..78f6a9d410d --- /dev/null +++ b/test/spec/modules/connatixBidAdapter_spec.js @@ -0,0 +1,350 @@ +import { expect } from 'chai'; +import { + spec, + getBidFloor as connatixGetBidFloor +} from '../../../modules/connatixBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; + +describe('connatixBidAdapter', function () { + let bid; + + function mockBidRequest() { + const mediaTypes = { + banner: { + sizes: [16, 9], + } + }; + return { + bidId: 'testing', + bidder: 'connatix', + params: { + placementId: '30e91414-545c-4f45-a950-0bec9308ff22' + }, + mediaTypes + }; + }; + + describe('isBidRequestValid', function () { + this.beforeEach(function () { + bid = mockBidRequest(); + }); + + it('Should return true if all required fileds are present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if bidder does not correspond', function () { + bid.bidder = 'abc'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if bidId is missing', function () { + delete bid.bidId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if params object is missing', function () { + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if placementId is missing from params', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if mediaTypes is missing', function () { + delete bid.mediaTypes; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if banner is missing from mediaTypes ', function () { + delete bid.mediaTypes.banner; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is missing from banner object', function () { + delete bid.mediaTypes.banner.sizes; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is not an array', function () { + bid.mediaTypes.banner.sizes = 'test'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is an empty array', function () { + bid.mediaTypes.banner.sizes = []; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return true if add an extra field was added to the bidRequest', function () { + bid.params.test = 1; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + }); + + describe('buildRequests', function () { + let serverRequest; + let bidderRequest = { + refererInfo: { + canonicalUrl: '', + numIframes: 0, + reachedTop: true, + referer: 'http://example.com', + stack: ['http://example.com'] + }, + gdprConsent: { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: {}, + gdprApplies: true + }, + uspConsent: '1YYY', + ortb2: { + site: { + data: { + pageType: 'article' + } + } + } + }; + + this.beforeEach(function () { + bid = mockBidRequest(); + serverRequest = spec.buildRequests([bid], bidderRequest); + }) + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://capi.connatix.com/rtb/hba'); + }); + it('Returns request payload', function () { + expect(serverRequest.data).to.not.empty; + }); + it('Validate request payload', function () { + expect(serverRequest.data.bidRequests[0].bidId).to.equal(bid.bidId); + expect(serverRequest.data.bidRequests[0].placementId).to.equal(bid.params.placementId); + expect(serverRequest.data.bidRequests[0].floor).to.equal(0); + expect(serverRequest.data.bidRequests[0].mediaTypes).to.equal(bid.mediaTypes); + expect(serverRequest.data.bidRequests[0].sizes).to.equal(bid.mediaTypes.sizes); + expect(serverRequest.data.refererInfo).to.equal(bidderRequest.refererInfo); + expect(serverRequest.data.gdprConsent).to.equal(bidderRequest.gdprConsent); + expect(serverRequest.data.uspConsent).to.equal(bidderRequest.uspConsent); + expect(serverRequest.data.ortb2).to.equal(bidderRequest.ortb2); + }); + }); + + describe('interpretResponse', function () { + const CustomerId = '99f20d18-c4b4-4a28-3d8e-d43e2c8cb4ac'; + const PlayerId = 'e4984e88-9ff4-45a3-8b9d-33aabcad634f'; + const Bid = {Cpm: 0.1, RequestId: '2f897340c4eaa3', Ttl: 86400, CustomerId, PlayerId}; + + let serverResponse; + this.beforeEach(function () { + serverResponse = { + body: { + Bids: [ Bid ] + }, + headers: function() { } + }; + }); + + it('Should return an empty array if Bids is null', function () { + serverResponse.body.Bids = null; + + const response = spec.interpretResponse(serverResponse); + expect(response).to.be.an('array').that.is.empty; + }); + + it('Should return an empty array if Bids is empty array', function () { + serverResponse.body.Bids = []; + const response = spec.interpretResponse(serverResponse); + expect(response).to.be.an('array').that.is.empty; + }); + + it('Should return one bid response for one bid', function() { + const bidResponses = spec.interpretResponse(serverResponse); + expect(bidResponses.length).to.equal(1); + }); + + it('Should contains the same values as in the serverResponse', function() { + const bidResponses = spec.interpretResponse(serverResponse); + + const [ bidResponse ] = bidResponses; + expect(bidResponse.requestId).to.equal(serverResponse.body.Bids[0].RequestId); + expect(bidResponse.cpm).to.equal(serverResponse.body.Bids[0].Cpm); + expect(bidResponse.ttl).to.equal(serverResponse.body.Bids[0].Ttl); + expect(bidResponse.currency).to.equal('USD'); + expect(bidResponse.mediaType).to.equal(BANNER); + expect(bidResponse.netRevenue).to.be.true; + }); + + it('Should return n bid responses for n bids', function() { + serverResponse.body.Bids = [ { ...Bid }, { ...Bid } ]; + + const firstBidCpm = 4; + serverResponse.body.Bids[0].Cpm = firstBidCpm; + + const secondBidCpm = 13; + serverResponse.body.Bids[1].Cpm = secondBidCpm; + + const bidResponses = spec.interpretResponse(serverResponse); + expect(bidResponses.length).to.equal(2); + + expect(bidResponses[0].cpm).to.equal(firstBidCpm); + expect(bidResponses[1].cpm).to.equal(secondBidCpm); + }); + }); + + describe('getUserSyncs', function() { + const CustomerId = '99f20d18-c4b4-4a28-3d8e-d43e2c8cb4ac'; + const PlayerId = 'e4984e88-9ff4-45a3-8b9d-33aabcad634f'; + const UserSyncEndpoint = 'https://connatix.com/sync' + const Bid = {Cpm: 0.1, RequestId: '2f897340c4eaa3', Ttl: 86400, CustomerId, PlayerId}; + + const serverResponse = { + body: { + UserSyncEndpoint, + Bids: [ Bid ] + }, + headers: function() { } + }; + + it('Should return an empty array when iframeEnabled: false', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when serverResponses is emprt array', function () { + expect(spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when iframeEnabled: true but serverResponses in an empty array', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [serverResponse], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when iframeEnabled: true but serverResponses in an not defined or null', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, undefined, {}, {}, {})).to.be.an('array').that.is.empty; + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, null, {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return one user sync object when iframeEnabled is true and serverResponses is not an empry array', function () { + expect(spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [serverResponse], {}, {}, {})).to.be.an('array').that.is.not.empty; + }); + it('Should return a list containing a single object having type: iframe and url: syncUrl', function () { + const userSyncList = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [serverResponse], undefined, undefined, undefined); + const { type, url } = userSyncList[0]; + expect(type).to.equal('iframe'); + expect(url).to.equal(UserSyncEndpoint); + }); + it('Should append gdpr: 0 if gdprConsent object is provided but gdprApplies field is not provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=0`); + }); + it('Should append gdpr having the value of gdprApplied if gdprConsent object is present and have gdprApplies field', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1`); + }); + it('Should append gdpr_consent if gdprConsent object is present and have gdprApplies field', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'alabala'}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=alabala`); + }); + it('Should encodeURI gdpr_consent corectly', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262`); + }); + it('Should append usp_consent to the url if uspConsent is provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + '1YYYN', + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262&us_privacy=1YYYN`); + }); + it('Should not modify the sync url if gppConsent param is provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + '1YYYN', + {consent: '1'} + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262&us_privacy=1YYYN`); + }); + }); + + describe('getBidFloor', function () { + this.beforeEach(function () { + bid = mockBidRequest(); + }); + + it('Should return 0 if both getFloor method and bidfloor param from bid are absent.', function () { + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(0); + }); + + it('Should return the value of the bidfloor parameter if the getFloor method is not defined but the bidfloor parameter is defined', function () { + const floorValue = 3; + bid.params.bidfloor = floorValue; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorValue); + }); + + it('Should return the value of the getFloor method if the getFloor method is defined but the bidfloor parameter is not defined', function () { + const floorValue = 7; + bid.getFloor = function() { + return { floor: floorValue }; + }; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorValue); + }); + + it('Should return the value of the getFloor method if both getFloor method and bidfloor parameter are defined', function () { + const floorParamValue = 3; + bid.params.bidfloor = floorParamValue; + + const floorMethodValue = 7; + bid.getFloor = function() { + return { floor: floorMethodValue }; + }; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorMethodValue); + }); + + it('Should return 0 if the getFloor method is defined and it crash when call it', function () { + bid.getFloor = function() { + throw new Error('error'); + }; + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/connectIdSystem_spec.js b/test/spec/modules/connectIdSystem_spec.js index 5376ba60886..686c3d63a63 100644 --- a/test/spec/modules/connectIdSystem_spec.js +++ b/test/spec/modules/connectIdSystem_spec.js @@ -3,6 +3,7 @@ import {connectIdSubmodule, storage} from 'modules/connectIdSystem.js'; import {server} from '../../mocks/xhr'; import {parseQS, parseUrl} from 'src/utils.js'; import {uspDataHandler, gppDataHandler} from 'src/adapterManager.js'; +import * as refererDetection from '../../../src/refererDetection'; const TEST_SERVER_URL = 'http://localhost:9876/'; @@ -288,6 +289,79 @@ describe('Yahoo ConnectID Submodule', () => { expect(setCookieStub.firstCall.args[2]).to.equal(expiryDelta.toUTCString()); }); + it('returns an object with the stored ID from cookies and syncs because of expired TTL', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 10000; + const cookieData = {connectId: 'foo', he: 'email', lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id', 'callback'); + expect(result.id).to.deep.equal(cookieData); + expect(typeof result.callback).to.equal('function'); + }); + + it('returns an object with the stored ID from cookies and not syncs because of valid TTL', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id'); + cookieData.lastUsed = result.id.lastUsed; + expect(result.id).to.deep.equal(cookieData); + }); + + it('returns an object with the stored ID from cookies and not syncs because of valid TTL with provided puid', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + puid: '9' + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id'); + cookieData.lastUsed = result.id.lastUsed; + expect(result.id).to.deep.equal(cookieData); + }); + + it('returns an object with the stored ID from cookies and syncs because is O&O traffic', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + const getRefererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); + getRefererInfoStub.returns({ + ref: 'https://dev.fc.yahoo.com?test' + }); + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + getRefererInfoStub.restore(); + + expect(result).to.be.an('object').that.has.all.keys('id', 'callback'); + expect(result.id).to.deep.equal(cookieData); + expect(typeof result.callback).to.equal('function'); + }); + it('Makes an ajax GET request to the production API endpoint with stored puid when id is stale', () => { const last15Days = Date.now() - (60 * 60 * 24 * 1000 * 15); const last29Days = Date.now() - (60 * 60 * 24 * 1000 * 29); diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js index 1170f418caf..99d4f94f502 100644 --- a/test/spec/modules/consentManagementGpp_spec.js +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -1,12 +1,23 @@ -import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, staticConsentData } from 'modules/consentManagementGpp.js'; -import { gppDataHandler } from 'src/adapterManager.js'; +import { + consentTimeout, + GPPClient, + requestBidsHook, + resetConsentData, + setConsentConfig, + userCMP +} from 'modules/consentManagementGpp.js'; +import {gppDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { config } from 'src/config.js'; +import {config} from 'src/config.js'; import 'src/prebid.js'; +import {MODE_CALLBACK, MODE_MIXED} from '../../../libraries/cmp/cmpClient.js'; +import {GreedyPromise} from '../../../src/utils/promise.js'; let expect = require('chai').expect; describe('consentManagementGpp', function () { + beforeEach(resetConsentData); + describe('setConsentConfig tests:', function () { describe('empty setConsentConfig value', function () { beforeEach(function () { @@ -99,7 +110,7 @@ describe('consentManagementGpp', function () { config.resetConfig(); }); - it('results in user settings overriding system defaults for v2 spec', () => { + it('results in user settings overriding system defaults', () => { let staticConfig = { gpp: { cmpApi: 'static', @@ -109,22 +120,621 @@ describe('consentManagementGpp', function () { gppString: 'ABCDEFG1234', gppVersion: 1, sectionId: 3, - sectionList: [] + sectionList: [], + parsedSections: { + usnat: [ + { + MockUsnatParsedFlag: true + }, + ] + }, } } }; setConsentConfig(staticConfig); expect(userCMP).to.be.equal('static'); - expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used const consent = gppDataHandler.getConsentData(); expect(consent.gppString).to.eql(staticConfig.gpp.consentData.gppString); - expect(consent.fullGppData).to.eql(staticConfig.gpp.consentData); - expect(staticConsentData).to.be.equal(staticConfig.gpp.consentData); + expect(consent.gppData).to.eql(staticConfig.gpp.consentData); + expect(consent.sectionData).to.eql(staticConfig.gpp.sectionData); + }); + }); + }); + describe('GPPClient.ping', () => { + function mkPingData(gppVersion) { + return { + gppVersion + } + } + Object.entries({ + 'unknown': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData(), + apiVersion: '1.1', + client({callback}) { + callback(this.pingData); + } + }, + '1.0': { + expectedMode: MODE_MIXED, + pingData: mkPingData('1.0'), + apiVersion: '1.0', + client() { + return this.pingData; + } + }, + '1.1 that runs callback immediately': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.1'), + apiVersion: '1.1', + client({callback}) { + callback(this.pingData); + } + }, + '1.1 that defers callback': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.1'), + apiVersion: '1.1', + client({callback}) { + setTimeout(() => callback(this.pingData), 10); + } + }, + '> 1.1': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.2'), + apiVersion: '1.1', + client({callback}) { + setTimeout(() => callback(this.pingData), 10); + } + } + }).forEach(([t, scenario]) => { + describe(`using CMP version ${t}`, () => { + let clients, mkClient; + beforeEach(() => { + clients = []; + mkClient = ({mode}) => { + const mockClient = function (args) { + if (args.command === 'ping') { + return Promise.resolve(scenario.client(args)); + } + } + mockClient.mode = mode; + mockClient.close = sinon.stub(); + clients.push(mockClient); + return mockClient; + } + }); + + it('should resolve to client with the correct mode', () => { + return GPPClient.ping(mkClient).then(([client]) => { + expect(client.cmp.mode).to.eql(scenario.expectedMode); + }); + }); + + it('should resolve to pingData', () => { + return GPPClient.ping(mkClient).then(([_, pingData]) => { + expect(pingData).to.eql(scenario.pingData); + }); + }); + + it('should .close the probing client', () => { + return GPPClient.ping(mkClient).then(([client]) => { + sinon.assert.called(clients[0].close); + sinon.assert.notCalled(client.cmp.close); + }) + }); + + it('should .tag the client with version', () => { + return GPPClient.ping(mkClient).then(([client]) => { + expect(client.apiVersion).to.eql(scenario.apiVersion); + }) + }) + }) + }); + + it('should reject when mkClient returns null (CMP not found)', () => { + return GPPClient.ping(() => null).catch((err) => { + expect(err.message).to.match(/not found/); + }); + }); + + it('should reject when client rejects', () => { + const err = {some: 'prop'}; + const mockClient = () => Promise.reject(err); + mockClient.close = sinon.stub(); + return GPPClient.ping(() => mockClient).catch((result) => { + expect(result).to.eql(err); + sinon.assert.called(mockClient.close); + }); + }); + + it('should reject when callback is invoked with success = false', () => { + const err = 'error'; + const mockClient = ({callback}) => callback(err, false); + mockClient.close = sinon.stub(); + return GPPClient.ping(() => mockClient).catch((result) => { + expect(result).to.eql(err); + sinon.assert.called(mockClient.close); + }) + }) + }); + + describe('GPPClient.init', () => { + let makeCmp, cmpCalls, cmpResult; + + beforeEach(() => { + cmpResult = {signalStatus: 'ready', gppString: 'mock-str'}; + cmpCalls = []; + makeCmp = sinon.stub().callsFake(() => { + function mockCmp(args) { + cmpCalls.push(args); + return GreedyPromise.resolve(cmpResult); + } + mockCmp.close = sinon.stub(); + return mockCmp; + }); + }); + + it('should re-use same client', (done) => { + GPPClient.init(makeCmp).then(([client]) => { + GPPClient.init(makeCmp).then(([client2, consentPm]) => { + expect(client2).to.equal(client); + expect(cmpCalls.filter((el) => el.command === 'ping').length).to.equal(2) // recycled client should be refreshed + consentPm.then((consent) => { + expect(consent.gppString).to.eql('mock-str'); + done() + }) + }); + }); + }); + + it('should not re-use errors', (done) => { + cmpResult = Promise.reject(new Error()); + GPPClient.init(makeCmp).catch(() => { + cmpResult = {signalStatus: 'ready'}; + return GPPClient.init(makeCmp).then(([client]) => { + expect(client).to.exist; + done() + }) + }) + }) + }) + + describe('GPP client', () => { + const CHANGE_EVENTS = ['sectionChange', 'signalStatus']; + + let gppClient, gppData, cmpReady, eventListener; + + function mockClient(apiVersion = '1.1', cmpVersion = '1.1') { + const mockCmp = sinon.stub().callsFake(function ({command, callback}) { + if (command === 'addEventListener') { + eventListener = callback; + } else { + throw new Error('unexpected command: ' + command); + } + }) + const client = new GPPClient(cmpVersion, mockCmp); + client.apiVersion = apiVersion; + client.getGPPData = sinon.stub().callsFake(() => Promise.resolve(gppData)); + client.isCMPReady = sinon.stub().callsFake(() => cmpReady); + client.events = CHANGE_EVENTS; + return client; + } + + beforeEach(() => { + gppDataHandler.reset(); + eventListener = null; + cmpReady = true; + gppData = { + applicableSections: [7], + gppString: 'mock-string', + parsedSections: { + usnat: [ + { + Field: 'val' + }, + { + SubsectionType: 1, + Gpc: false + } + ] + } + }; + gppClient = mockClient(); + }); + + describe('updateConsent', () => { + it('should update data handler with consent data', () => { + return gppClient.updateConsent().then(data => { + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + expect(gppDataHandler.ready).to.be.true; + }); + }); + + Object.entries({ + 'emtpy': {}, + 'missing': null + }).forEach(([t, data]) => { + it(`should not update, and reject promise, when gpp data is ${t}`, (done) => { + gppData = data; + gppClient.updateConsent().catch(err => { + expect(err.message).to.match(/empty/); + expect(err.args).to.eql(data == null ? [] : [data]); + expect(gppDataHandler.ready).to.be.false; + done() + }) + }); + }) + + it('should not update when gpp data rejects', (done) => { + gppData = Promise.reject(new Error('err')); + gppClient.updateConsent().catch(err => { + expect(gppDataHandler.ready).to.be.false; + expect(err.message).to.eql('err'); + done(); + }) + }); + + describe('consent data validation', () => { + Object.entries({ + applicableSections: { + 'not an array': 'not-an-array', + }, + gppString: { + 'not a string': 234 + }, + parsedSections: { + 'not an object': 'not-an-object' + } + }).forEach(([prop, tests]) => { + describe(`validation: when ${prop} is`, () => { + Object.entries(tests).forEach(([t, value]) => { + describe(t, () => { + it('should not update', (done) => { + Object.assign(gppData, {[prop]: value}); + gppClient.updateConsent().catch(err => { + expect(err.message).to.match(/unexpected/); + expect(err.args).to.eql([gppData]); + expect(gppDataHandler.ready).to.be.false; + done(); + }); + }); + }) + }); + }); + }); + }); + }); + + describe('init', () => { + beforeEach(() => { + gppClient.isCMPReady = function (pingData) { + return pingData.ready; + } + gppClient.getGPPData = function (pingData) { + return Promise.resolve(pingData); + } + }) + + it('does not use initial pingData if CMP is not ready', () => { + gppClient.init({...gppData, ready: false}); + expect(eventListener).to.exist; + expect(gppDataHandler.ready).to.be.false; + }); + + it('uses initial pingData (and resolves promise) if CMP is ready', () => { + return gppClient.init({...gppData, ready: true}).then(data => { + expect(eventListener).to.exist; + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + }) + }); + + it('rejects promise when CMP errors out', (done) => { + gppClient.init({ready: false}).catch((err) => { + expect(err.message).to.match(/error/); + expect(err.args).to.eql(['error']) + done(); + }); + eventListener('error', false); }); + + Object.entries({ + 'empty': {}, + 'null': null, + 'irrelevant': {eventName: 'irrelevant'} + }).forEach(([t, evt]) => { + it(`ignores ${t} events`, () => { + let pm = gppClient.init({ready: false}).catch((err) => err.args[0] !== 'done' && Promise.reject(err)); + eventListener(evt); + eventListener('done', false); + return pm; + }) + }); + + it('rejects the promise when cmpStatus is "error"', (done) => { + const evt = {eventName: 'other', pingData: {cmpStatus: 'error'}}; + gppClient.init({ready: false}).catch(err => { + expect(err.message).to.match(/error/); + expect(err.args).to.eql([evt]); + done(); + }); + eventListener(evt); + }) + + CHANGE_EVENTS.forEach(evt => { + describe(`event: ${evt}`, () => { + function makeEvent(pingData) { + return { + eventName: evt, + pingData + } + } + + let gppData2 + beforeEach(() => { + gppData2 = Object.assign(gppData, {gppString: '2nd'}); + }); + + it('does not fire consent data updates if the CMP is not ready', (done) => { + gppClient.init({ready: false}).catch(() => { + expect(gppDataHandler.ready).to.be.false; + done(); + }); + eventListener({...gppData2, ready: false}); + eventListener('done', false); + }) + + it('fires consent data updates (and resolves promise) if CMP is ready', (done) => { + gppClient.init({ready: false}).then(data => { + sinon.assert.match(data, gppData2); + done() + }); + cmpReady = true; + eventListener(makeEvent({...gppData2, ready: true})); + }); + + it('keeps updating consent data on new events', () => { + let pm = gppClient.init({ready: false}).then(data => { + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + }); + eventListener(makeEvent({...gppData, ready: true})); + return pm.then(() => { + eventListener(makeEvent({...gppData2, ready: true})) + }).then(() => { + sinon.assert.match(gppDataHandler.getConsentData(), gppData2); + }); + }); + }) + }) }); }); + describe('GPP 1.0 protocol', () => { + let mockCmp, gppClient; + beforeEach(() => { + mockCmp = sinon.stub(); + gppClient = new (GPPClient.getClient('1.0'))('1.0', mockCmp); + }); + + describe('isCMPReady', () => { + Object.entries({ + 'loaded': [true, 'loaded'], + 'other': [false, 'other'], + 'undefined': [false, undefined] + }).forEach(([t, [expected, cmpStatus]]) => { + it(`should be ${expected} when cmpStatus is ${t}`, () => { + expect(gppClient.isCMPReady(Object.assign({}, {cmpStatus}))).to.equal(expected); + }); + }); + }); + + describe('getGPPData', () => { + let gppData, pingData; + beforeEach(() => { + gppData = { + gppString: 'mock-string', + supportedAPIs: ['usnat'], + applicableSections: [7, 8] + } + pingData = { + supportedAPIs: gppData.supportedAPIs + }; + }); + + function mockCmpCommands(commands) { + mockCmp.callsFake(({command, parameter}) => { + if (commands.hasOwnProperty((command))) { + return Promise.resolve(commands[command](parameter)); + } else { + return Promise.reject(new Error(`unrecognized command ${command}`)) + } + }) + } + + it('should retrieve consent string and applicableSections', () => { + mockCmpCommands({ + getGPPData: () => gppData + }) + return gppClient.getGPPData(pingData).then(data => { + sinon.assert.match(data, gppData); + }) + }); + + it('should reject when getGPPData rejects', (done) => { + mockCmpCommands({ + getGPPData: () => Promise.reject(new Error('err')) + }); + gppClient.getGPPData(pingData).catch(err => { + expect(err.message).to.eql('err'); + done(); + }); + }); + + it('should not choke if supportedAPIs is missing', () => { + [gppData, pingData].forEach(ob => { delete ob.supportedAPIs; }) + mockCmpCommands({ + getGPPData: () => gppData + }); + return gppClient.getGPPData(pingData).then(res => { + expect(res.gppString).to.eql(gppData.gppString); + expect(res.parsedSections).to.eql({}); + }) + }) + + describe('section data', () => { + let usnat, parsedUsnat; + + function mockSections(sections) { + mockCmpCommands({ + getGPPData: () => gppData, + getSection: (api) => (sections[api]) + }); + }; + + beforeEach(() => { + usnat = { + MockField: 'val', + OtherField: 'o', + Gpc: true + }; + parsedUsnat = [ + { + MockField: 'val', + OtherField: 'o' + }, + { + SubsectionType: 1, + Gpc: true + } + ] + }); + + it('retrieves section data', () => { + mockSections({usnat}); + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}) + }); + }); + + it('does not choke if a section is missing', () => { + mockSections({usnat}); + gppData.supportedAPIs = ['usnat', 'missing']; + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}); + }) + }); + + it('does not choke if a section fails', () => { + mockSections({usnat, err: Promise.reject(new Error('err'))}); + gppData.supportedAPIs = ['usnat', 'err']; + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}); + }) + }); + }) + }); + }); + + describe('GPP 1.1 protocol', () => { + let mockCmp, gppClient; + beforeEach(() => { + mockCmp = sinon.stub(); + gppClient = new (GPPClient.getClient('1.1'))('1.1', mockCmp); + }); + + describe('isCMPReady', () => { + Object.entries({ + 'ready': [true, 'ready'], + 'not ready': [false, 'not ready'], + 'undefined': [false, undefined] + }).forEach(([t, [expected, signalStatus]]) => { + it(`should be ${expected} when signalStatus is ${t}`, () => { + expect(gppClient.isCMPReady(Object.assign({}, {signalStatus}))).to.equal(expected); + }); + }); + }); + + it('gets GPPData from pingData', () => { + mockCmp.throws(new Error()); + const pingData = { + 'gppVersion': '1.1', + 'cmpStatus': 'loaded', + 'cmpDisplayStatus': 'disabled', + 'supportedAPIs': [ + '5:tcfcav1', + '7:usnat', + '8:usca', + '9:usva', + '10:usco', + '11:usut', + '12:usct' + ], + 'signalStatus': 'ready', + 'cmpId': 31, + 'sectionList': [ + 7 + ], + 'applicableSections': [ + 7 + ], + 'gppString': 'DBABL~BAAAAAAAAgA.QA', + 'parsedSections': { + 'usnat': [ + { + 'Version': 1, + 'SharingNotice': 0, + 'SaleOptOutNotice': 0, + 'SharingOptOutNotice': 0, + 'TargetedAdvertisingOptOutNotice': 0, + 'SensitiveDataProcessingOptOutNotice': 0, + 'SensitiveDataLimitUseNotice': 0, + 'SaleOptOut': 0, + 'SharingOptOut': 0, + 'TargetedAdvertisingOptOut': 0, + 'SensitiveDataProcessing': [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + 'KnownChildSensitiveDataConsents': [ + 0, + 0 + ], + 'PersonalDataConsents': 0, + 'MspaCoveredTransaction': 2, + 'MspaOptOutOptionMode': 0, + 'MspaServiceProviderMode': 0 + }, + { + 'SubsectionType': 1, + 'Gpc': false + } + ] + } + }; + return gppClient.getGPPData(pingData).then((gppData) => { + sinon.assert.match(gppData, { + gppString: pingData.gppString, + applicableSections: pingData.applicableSections, + parsedSections: pingData.parsedSections + }) + }) + }) + }) + describe('requestBidsHook tests:', function () { let goodConfig = { gpp: { @@ -228,13 +838,13 @@ describe('consentManagementGpp', function () { }); it('should continue the auction immediately, without consent data, if timeout is 0', (done) => { + window.__gpp = function () {}; setConsentConfig({ gpp: { cmpApi: 'iab', timeout: 0 } }); - window.__gpp = function () {}; try { requestBidsHook(() => { const consent = gppDataHandler.getConsentData(); @@ -251,14 +861,16 @@ describe('consentManagementGpp', function () { describe('already known consentData:', function () { let cmpStub = sinon.stub(); - function mockCMP(cmpResponse) { - return function (...args) { - if (args[0] === 'addEventListener') { - args[1](({ - eventName: 'sectionChange' - })); - } else if (args[0] === 'getGPPData') { - return cmpResponse; + function mockCMP(pingData) { + return function (command, callback) { + switch (command) { + case 'addEventListener': + // eslint-disable-next-line standard/no-callback-literal + callback({eventName: 'sectionChange', pingData}) + break; + case 'ping': + callback(pingData) + break; } } } @@ -281,7 +893,7 @@ describe('consentManagementGpp', function () { gppString: 'xyz', }; - cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP(testConsentData)); + cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP({...testConsentData, signalStatus: 'ready'})); setConsentConfig(goodConfig); requestBidsHook(() => {}, {}); cmpStub.reset(); @@ -297,281 +909,5 @@ describe('consentManagementGpp', function () { sinon.assert.notCalled(cmpStub); }); }); - - describe('iframe tests', function () { - let cmpPostMessageCb = () => {}; - let stringifyResponse; - - function createIFrameMarker(frameName) { - let ifr = document.createElement('iframe'); - ifr.width = 0; - ifr.height = 0; - ifr.name = frameName; - document.body.appendChild(ifr); - return ifr; - } - - function creatCmpMessageHandler(prefix, returnEvtValue, returnGPPValue) { - return function (event) { - if (event && event.data) { - let data = event.data; - if (data[`${prefix}Call`]) { - let callId = data[`${prefix}Call`].callId; - let response; - if (data[`${prefix}Call`].command === 'addEventListener') { - response = { - [`${prefix}Return`]: { - callId, - returnValue: returnEvtValue, - success: true - } - } - } else if (data[`${prefix}Call`].command === 'getGPPData') { - response = { - [`${prefix}Return`]: { - callId, - returnValue: returnGPPValue, - success: true - } - } - } - event.source.postMessage(stringifyResponse ? JSON.stringify(response) : response, '*'); - } - } - } - } - - function testIFramedPage(testName, messageFormatString, tarConsentString, tarSections) { - it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { - stringifyResponse = messageFormatString; - setConsentConfig(goodConfig); - requestBidsHook(() => { - let consent = gppDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logError); - expect(consent.gppString).to.equal(tarConsentString); - expect(consent.applicableSections).to.deep.equal(tarSections); - done(); - }, {}); - }); - } - - beforeEach(function () { - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); - }); - - afterEach(function () { - utils.logError.restore(); - utils.logWarn.restore(); - config.resetConfig(); - resetConsentData(); - }); - - describe('v2 CMP workflow for iframe pages:', function () { - stringifyResponse = false; - let ifr2 = null; - - beforeEach(function () { - ifr2 = createIFrameMarker('__gppLocator'); - cmpPostMessageCb = creatCmpMessageHandler('__gpp', { - eventName: 'sectionChange' - }, { - gppString: 'abc12345234', - applicableSections: [7] - }); - window.addEventListener('message', cmpPostMessageCb, false); - }); - - afterEach(function () { - delete window.__gpp; // deletes the local copy made by the postMessage CMP call function - document.body.removeChild(ifr2); - window.removeEventListener('message', cmpPostMessageCb); - }); - - testIFramedPage('with/JSON response', false, 'abc12345234', [7]); - testIFramedPage('with/String response', true, 'abc12345234', [7]); - }); - }); - - describe('direct calls to CMP API tests', function () { - let cmpStub = sinon.stub(); - - beforeEach(function () { - didHookReturn = false; - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); - }); - - afterEach(function () { - config.resetConfig(); - cmpStub.restore(); - utils.logError.restore(); - utils.logWarn.restore(); - resetConsentData(); - }); - - describe('v2 CMP workflow for normal pages:', function () { - beforeEach(function () { - window.__gpp = function () {}; - }); - - afterEach(function () { - delete window.__gpp; - }); - - it('performs lookup check and stores consentData for a valid existing user', function () { - let testConsentData = { - gppString: 'abc12345234', - applicableSections: [7] - }; - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); - let consent = gppDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logError); - expect(didHookReturn).to.be.true; - expect(consent.gppString).to.equal(testConsentData.gppString); - expect(consent.applicableSections).to.deep.equal(testConsentData.applicableSections); - }); - - it('produces gdpr metadata', function () { - let testConsentData = { - gppString: 'abc12345234', - applicableSections: [7] - }; - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); - let consentMeta = gppDataHandler.getConsentMeta(); - sinon.assert.notCalled(utils.logError); - expect(consentMeta.generatedAt).to.be.above(1644367751709); - }); - - it('throws an error when processCmpData check fails + does not call requestBids callback', function () { - let testConsentData = {}; - let bidsBackHandlerReturn = false; - - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - sinon.assert.notCalled(utils.logWarn); - sinon.assert.notCalled(utils.logError); - - [utils.logWarn, utils.logError].forEach((stub) => stub.reset()); - - requestBidsHook(() => { - didHookReturn = true; - }, { - bidsBackHandler: () => bidsBackHandlerReturn = true - }); - let consent = gppDataHandler.getConsentData(); - - sinon.assert.calledOnce(utils.logError); - sinon.assert.notCalled(utils.logWarn); - expect(didHookReturn).to.be.false; - expect(bidsBackHandlerReturn).to.be.true; - expect(consent).to.be.null; - expect(gppDataHandler.ready).to.be.true; - }); - - describe('when proper consent is not available', () => { - let gppStub; - - function runAuction() { - setConsentConfig({ - gpp: { - cmpApi: 'iab', - timeout: 10, - } - }); - return new Promise((resolve, reject) => { - requestBidsHook(() => { - didHookReturn = true; - }, {}); - setTimeout(() => didHookReturn ? resolve() : reject(new Error('Auction did not run')), 20); - }) - } - - function mockGppCmp(gppdata) { - gppStub.callsFake((api, cb) => { - if (api === 'addEventListener') { - // eslint-disable-next-line standard/no-callback-literal - cb({ - pingData: { - cmpStatus: 'loaded' - } - }, true); - } - if (api === 'getGPPData') { - return gppdata; - } - }); - } - - beforeEach(() => { - gppStub = sinon.stub(window, '__gpp'); - }); - - afterEach(() => { - gppStub.restore(); - }) - - it('should continue auction with null consent when CMP is unresponsive', () => { - return runAuction().then(() => { - const consent = gppDataHandler.getConsentData(); - expect(consent.applicableSections).to.deep.equal([]); - expect(consent.gppString).to.be.undefined; - expect(gppDataHandler.ready).to.be.true; - }); - }); - - it('should use consent provided by events other than sectionChange', () => { - mockGppCmp({ - gppString: 'mock-consent-string', - applicableSections: [7] - }); - return runAuction().then(() => { - const consent = gppDataHandler.getConsentData(); - expect(consent.applicableSections).to.deep.equal([7]); - expect(consent.gppString).to.equal('mock-consent-string'); - expect(gppDataHandler.ready).to.be.true; - }); - }); - }); - }); - }); }); }); diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index e98486754ab..c372c66f7f0 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -522,6 +522,19 @@ describe('consentManagement', function () { setConsentConfig(goodConfig); expect(uspDataHandler.getConsentData()).to.eql('string'); }); + + it('does not invoke registerDeletion if the CMP calls back with an error', () => { + sandbox.stub(window, '__uspapi').callsFake((cmd, _, cb) => { + if (cmd === 'registerDeletion') { + cb(null, false); + } else { + // eslint-disable-next-line standard/no-callback-literal + cb({uspString: 'string'}, true); + } + }); + setConsentConfig(goodConfig); + sinon.assert.notCalled(adapterManager.callDataDeletionRequest); + }) }); }); }); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 2cd3d011e1d..c1ed042a2c8 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -1,18 +1,17 @@ import { - setConsentConfig, - requestBidsHook, - resetConsentData, - userCMP, - consentTimeout, actionTimeout, - staticConsentData, + consentTimeout, gdprScope, loadConsentData, - setActionTimeout + requestBidsHook, + resetConsentData, + setConsentConfig, + staticConsentData, + userCMP } from 'modules/consentManagement.js'; -import { gdprDataHandler } from 'src/adapterManager.js'; +import {gdprDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { config } from 'src/config.js'; +import {config} from 'src/config.js'; import 'src/prebid.js'; let expect = require('chai').expect; @@ -487,15 +486,6 @@ describe('consentManagement', function () { testIFramedPage('with/JSON response', false, 'abc12345234', 2); testIFramedPage('with/String response', true, 'abc12345234', 2); - - it('should contain correct v2 CMP definition', (done) => { - setConsentConfig(goodConfig); - requestBidsHook(() => { - const nbArguments = window.__tcfapi.toString().split('\n')[0].split(', ').length; - expect(nbArguments).to.equal(4); - done(); - }, {}); - }); }); }); diff --git a/test/spec/modules/consumableBidAdapter_spec.js b/test/spec/modules/consumableBidAdapter_spec.js index 556dce447b9..d8e75454245 100644 --- a/test/spec/modules/consumableBidAdapter_spec.js +++ b/test/spec/modules/consumableBidAdapter_spec.js @@ -53,6 +53,10 @@ const BIDDER_REQUEST_1 = { consentString: 'consent-test', gdprApplies: false }, + gppConsent: { + applicableSections: [1, 2], + gppString: 'consent-string' + }, refererInfo: { referer: 'http://example.com/page.html', reachedTop: true, @@ -647,10 +651,30 @@ describe('Consumable BidAdapter', function () { expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gdpr=0&gdpr_consent=GDPR_CONSENT_STRING'); }) - it('should return a sync url if iframe syncs are enabled and USP applies', function () { - let uspConsent = { - consentString: 'USP_CONSENT_STRING', + it('should return a sync url if iframe syncs are enabled and has GPP consent with applicable sections', function () { + let gppConsent = { + applicableSections: [1, 2], + gppString: 'GPP_CONSENT_STRING' + } + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, '', gppConsent); + + expect(opts.length).to.equal(1); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gpp=GPP_CONSENT_STRING&gpp_sid=1%2C2'); + }) + + it('should return a sync url if iframe syncs are enabled and has GPP consent without applicable sections', function () { + let gppConsent = { + applicableSections: [], + gppString: 'GPP_CONSENT_STRING' } + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, '', gppConsent); + + expect(opts.length).to.equal(1); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gpp=GPP_CONSENT_STRING'); + }) + + it('should return a sync url if iframe syncs are enabled and USP applies', function () { + let uspConsent = 'USP_CONSENT_STRING'; let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, uspConsent); expect(opts.length).to.equal(1); @@ -662,9 +686,7 @@ describe('Consumable BidAdapter', function () { consentString: 'GDPR_CONSENT_STRING', gdprApplies: true, } - let uspConsent = { - consentString: 'USP_CONSENT_STRING', - } + let uspConsent = 'USP_CONSENT_STRING'; let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], gdprConsent, uspConsent); expect(opts.length).to.equal(1); @@ -689,50 +711,22 @@ describe('Consumable BidAdapter', function () { sandbox.restore(); }); - it('Request should have unifiedId config params', function() { + it('Request should have EIDs', function() { bidderRequest.bidRequest[0].userId = {}; bidderRequest.bidRequest[0].userId.tdid = 'TTD_ID'; - bidderRequest.bidRequest[0].userIdAsEids = createEidsArray(bidderRequest.bidRequest[0].userId); - let request = spec.buildRequests(bidderRequest.bidRequest, BIDDER_REQUEST_1); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ + bidderRequest.bidRequest[0].userIdAsEids = [{ 'source': 'adserver.org', 'uids': [{ - 'id': 'TTD_ID', + 'id': 'TTD_ID_FROM_USER_ID_MODULE', 'atype': 1, 'ext': { 'rtiPartner': 'TDID' } }] - }]); - }); - - it('Request should have adsrvrOrgId from UserId Module if config and userId module both have TTD ID', function() { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - adsrvrOrgId: { - 'TDID': 'TTD_ID_FROM_CONFIG', - 'TDID_LOOKUP': 'TRUE', - 'TDID_CREATED_AT': '2022-06-21T09:47:00' - } - }; - return config[key]; - }); - bidderRequest.bidRequest[0].userId = {}; - bidderRequest.bidRequest[0].userId.tdid = 'TTD_ID'; - bidderRequest.bidRequest[0].userIdAsEids = createEidsArray(bidderRequest.bidRequest[0].userId); + }]; let request = spec.buildRequests(bidderRequest.bidRequest, BIDDER_REQUEST_1); let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'adserver.org', - 'uids': [{ - 'id': 'TTD_ID', - 'atype': 1, - 'ext': { - 'rtiPartner': 'TDID' - } - }] - }]); + expect(data.user.eids).to.deep.equal(bidderRequest.bidRequest[0].userIdAsEids); }); it('Request should NOT have adsrvrOrgId params if userId is NOT object', function() { diff --git a/test/spec/modules/conversantAnalyticsAdapter_spec.js b/test/spec/modules/conversantAnalyticsAdapter_spec.js index ce134f7f6af..f425535ce73 100644 --- a/test/spec/modules/conversantAnalyticsAdapter_spec.js +++ b/test/spec/modules/conversantAnalyticsAdapter_spec.js @@ -3,6 +3,7 @@ import {expect} from 'chai'; import {default as conversantAnalytics, CNVR_CONSTANTS, cnvrHelper} from 'modules/conversantAnalyticsAdapter'; import * as utils from 'src/utils.js'; import * as prebidGlobal from 'src/prebidGlobal'; +import {server} from '../../mocks/xhr.js'; import constants from 'src/constants.json' @@ -10,14 +11,13 @@ let events = require('src/events'); describe('Conversant analytics adapter tests', function() { let sandbox; // sinon sandbox to make restoring all stubbed objects easier - let xhr; // xhr stub from sinon for capturing data sent via ajax let clock; // clock stub from sinon to mock our cache cleanup interval let logInfoStub; const PREBID_VERSION = '1.2'; const SITE_ID = 108060; - let requests = []; + let requests; const DATESTAMP = Date.now(); const VALID_CONFIGURATION = { @@ -36,10 +36,9 @@ describe('Conversant analytics adapter tests', function() { }; beforeEach(function () { + requests = server.requests; sandbox = sinon.sandbox.create(); sandbox.stub(events, 'getEvents').returns([]); // need to stub this otherwise unwanted events seem to get fired during testing - xhr = sandbox.useFakeXMLHttpRequest(); // allows us to capture ajax requests - xhr.onCreate = function (req) { requests.push(req); }; // save ajax requests in a private array for testing purposes let getGlobalStub = { version: PREBID_VERSION, getUserIds: function() { // userIdTargeting.js init() gets called on AUCTION_END so we need to mock this function. @@ -60,7 +59,6 @@ describe('Conversant analytics adapter tests', function() { afterEach(function () { sandbox.restore(); - requests = []; // clean up any requests in our ajax request capture array. conversantAnalytics.disableAnalytics(); }); diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index b2f3d64a156..97b80ce95db 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -9,11 +9,12 @@ import { } from 'modules/criteoBidAdapter.js'; import * as utils from 'src/utils.js'; import * as refererDetection from 'src/refererDetection.js'; +import * as ajax from 'src/ajax.js'; import { config } from '../../../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../../../src/mediaTypes.js'; describe('The Criteo bidding adapter', function () { - let utilsMock, sandbox; + let utilsMock, sandbox, ajaxStub; beforeEach(function () { $$PREBID_GLOBAL$$.bidderSettings = { @@ -26,6 +27,7 @@ describe('The Criteo bidding adapter', function () { utilsMock = sinon.mock(utils); sandbox = sinon.sandbox.create(); + ajaxStub = sandbox.stub(ajax, 'ajax'); }); afterEach(function () { @@ -33,6 +35,7 @@ describe('The Criteo bidding adapter', function () { global.Criteo = undefined; utilsMock.restore(); sandbox.restore(); + ajaxStub.restore(); }); describe('getUserSyncs', function () { @@ -56,7 +59,9 @@ describe('The Criteo bidding adapter', function () { cookiesAreEnabledStub, localStorageIsEnabledStub, getCookieStub, - getDataFromLocalStorageStub; + setCookieStub, + getDataFromLocalStorageStub, + removeDataFromLocalStorageStub; beforeEach(function () { getConfigStub = sinon.stub(config, 'getConfig'); @@ -75,8 +80,10 @@ describe('The Criteo bidding adapter', function () { localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); localStorageIsEnabledStub.returns(true); - getCookieStub = sinon.stub(storage, 'getCookie') + getCookieStub = sinon.stub(storage, 'getCookie'); + setCookieStub = sinon.stub(storage, 'setCookie'); getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + removeDataFromLocalStorageStub = sinon.stub(storage, 'removeDataFromLocalStorage'); }); afterEach(function () { @@ -86,7 +93,9 @@ describe('The Criteo bidding adapter', function () { cookiesAreEnabledStub.restore(); localStorageIsEnabledStub.restore(); getCookieStub.restore(); + setCookieStub.restore(); getDataFromLocalStorageStub.restore(); + removeDataFromLocalStorageStub.restore(); }); it('should not trigger sync if publisher is using fast bid', function () { @@ -198,6 +207,46 @@ describe('The Criteo bidding adapter', function () { url: `https://gum.criteo.com/syncframe?origin=criteoPrebidAdapter&topUrl=www.abc.com&us_privacy=ABC#${JSON.stringify(expectedHash).replace(/"/g, '%22')}` }]); }); + + it('should delete user data when calling onDataDeletionRequest', () => { + const cookieData = { + 'cto_bundle': 'a' + }; + const lsData = { + 'cto_bundle': 'a' + } + getCookieStub.callsFake(cookieName => cookieData[cookieName]); + setCookieStub.callsFake((cookieName, value, expires) => cookieData[cookieName] = value); + getDataFromLocalStorageStub.callsFake(name => lsData[name]); + removeDataFromLocalStorageStub.callsFake(name => lsData[name] = ''); + spec.onDataDeletionRequest([]); + expect(getCookieStub.calledOnce).to.equal(true); + expect(setCookieStub.calledOnce).to.equal(true); + expect(getDataFromLocalStorageStub.calledOnce).to.equal(true); + expect(removeDataFromLocalStorageStub.calledOnce).to.equal(true); + expect(cookieData.cto_bundle).to.equal(''); + expect(lsData.cto_bundle).to.equal(''); + expect(ajaxStub.calledOnce).to.equal(true); + }); + + it('should not call API when calling onDataDeletionRequest with no id', () => { + const cookieData = { + 'cto_bundle': '' + }; + const lsData = { + 'cto_bundle': '' + } + getCookieStub.callsFake(cookieName => cookieData[cookieName]); + setCookieStub.callsFake((cookieName, value, expires) => cookieData[cookieName] = value); + getDataFromLocalStorageStub.callsFake(name => lsData[name]); + removeDataFromLocalStorageStub.callsFake(name => lsData[name] = ''); + spec.onDataDeletionRequest([]); + expect(getCookieStub.calledOnce).to.be.true; + expect(setCookieStub.called).to.be.false; + expect(getDataFromLocalStorageStub.calledOnce).to.be.true + expect(removeDataFromLocalStorageStub.called).to.be.false; + expect(ajaxStub.called).to.be.false; + }); }); describe('isBidRequestValid', function () { @@ -654,6 +703,33 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.source.tid).to.equal('abc'); }); + it('should properly transmit bidId if available', function () { + const bidderRequest = { + ortb2: { + source: { + tid: 'abc' + } + } + }; + const bidRequests = [ + { + bidId: 'bidId', + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: {} + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const ortbRequest = request.data; + expect(ortbRequest.slots[0].slotid).to.equal('bidId'); + }); + it('should properly build a request if refererInfo is not provided', function () { const bidderRequest = {}; const bidRequests = [ @@ -1250,12 +1326,25 @@ describe('The Criteo bidding adapter', function () { sizes: [[640, 480]], mediaTypes: { video: { + context: 'instream', playerSize: [640, 480], mimes: ['video/mp4', 'video/x-flv'], maxduration: 30, api: [1, 2], protocols: [2, 3], - plcmt: 3 + plcmt: 3, + w: 640, + h: 480, + linearity: 1, + skipmin: 30, + skipafter: 30, + minbitrate: 10000, + maxbitrate: 48000, + delivery: [1, 2, 3], + pos: 1, + playbackend: 1, + adPodDurationSec: 30, + durationRangeSec: [1, 30], } }, params: { @@ -1274,6 +1363,7 @@ describe('The Criteo bidding adapter', function () { expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d/); expect(request.method).to.equal('POST'); const ortbRequest = request.data; + expect(ortbRequest.slots[0].video.context).to.equal('instream'); expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); expect(ortbRequest.slots[0].sizes).to.deep.equal([]); expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['640x480']); @@ -1286,6 +1376,18 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.slots[0].video.playbackmethod).to.deep.equal([1, 3]); expect(ortbRequest.slots[0].video.placement).to.equal(2); expect(ortbRequest.slots[0].video.plcmt).to.equal(3); + expect(ortbRequest.slots[0].video.w).to.equal(640); + expect(ortbRequest.slots[0].video.h).to.equal(480); + expect(ortbRequest.slots[0].video.linearity).to.equal(1); + expect(ortbRequest.slots[0].video.skipmin).to.equal(30); + expect(ortbRequest.slots[0].video.skipafter).to.equal(30); + expect(ortbRequest.slots[0].video.minbitrate).to.equal(10000); + expect(ortbRequest.slots[0].video.maxbitrate).to.equal(48000); + expect(ortbRequest.slots[0].video.delivery).to.deep.equal([1, 2, 3]); + expect(ortbRequest.slots[0].video.pos).to.equal(1); + expect(ortbRequest.slots[0].video.playbackend).to.equal(1); + expect(ortbRequest.slots[0].video.adPodDurationSec).to.equal(30); + expect(ortbRequest.slots[0].video.durationRangeSec).to.deep.equal([1, 30]); }); it('should properly build a video request with more than one player size', function () { @@ -1803,6 +1905,70 @@ describe('The Criteo bidding adapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.slots[0].rwdd).to.be.undefined; }); + + it('should properly build a request when FLEDGE is enabled', function () { + const bidderRequest = { + fledgeEnabled: true, + }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + }, + ortb2Imp: { + ext: { + ae: 1 + } + } + }, + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.slots[0].ext.ae).to.equal(1); + }); + + it('should properly build a request when FLEDGE is disabled', function () { + const bidderRequest = { + fledgeEnabled: false, + }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + }, + ortb2Imp: { + ext: { + ae: 1 + } + } + }, + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.slots[0].ext).to.not.have.property('ae'); + }); }); describe('interpretResponse', function () { @@ -1837,6 +2003,11 @@ describe('The Criteo bidding adapter', function () { bidRequests: [{ adUnitCode: 'test-requestId', bidId: 'test-bidId', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { networkId: 456, } @@ -1855,6 +2026,202 @@ describe('The Criteo bidding adapter', function () { expect(bids[0].meta.networkName).to.equal('Criteo'); }); + it('should properly parse a bid response with a networkId with twin ad unit banner win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + cpm: 1.23, + creative: 'test-ad', + creativecode: 'test-crId', + width: 728, + height: 90, + deal: 'myDealCode', + adomain: ['criteo.com'], + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [640, 480], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId2'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].ad).to.equal('test-ad'); + expect(bids[0].creativeId).to.equal('test-crId'); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].dealId).to.equal('myDealCode'); + expect(bids[0].meta.advertiserDomains[0]).to.equal('criteo.com'); + expect(bids[0].meta.networkName).to.equal('Criteo'); + }); + + it('should properly parse a bid response with a networkId with twin ad unit video win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + bidId: 'abc123', + cpm: 1.23, + displayurl: 'http://test-ad', + width: 728, + height: 90, + zoneid: 123, + video: true, + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [728, 90], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].vastUrl).to.equal('http://test-ad'); + expect(bids[0].mediaType).to.equal(VIDEO); + }); + + it('should properly parse a bid response with a networkId with twin ad unit native win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + cpm: 1.23, + creative: 'test-ad', + creativecode: 'test-crId', + width: 728, + height: 90, + deal: 'myDealCode', + adomain: ['criteo.com'], + native: { + 'products': [{ + 'sendTargetingKeys': false, + 'title': 'Product title', + 'description': 'Product desc', + 'price': '100', + 'click_url': 'https://product.click', + 'image': { + 'url': 'https://publisherdirect.criteo.com/publishertag/preprodtest/creative.png', + 'height': 300, + 'width': 300 + }, + 'call_to_action': 'Try it now!' + }], + 'advertiser': { + 'description': 'sponsor', + 'domain': 'criteo.com', + 'logo': { 'url': 'https://www.criteo.com/images/criteo-logo.svg', 'height': 300, 'width': 300 } + }, + 'privacy': { + 'optout_click_url': 'https://info.criteo.com/privacy/informations', + 'optout_image_url': 'https://static.criteo.net/flash/icon/nai_small.png', + }, + 'impression_pixels': [{ 'url': 'https://my-impression-pixel/test/impression' }, { 'url': 'https://cas.com/lg.com' }] + }, + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + native: {} + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].mediaType).to.equal(NATIVE); + }); + it('should properly parse a bid response with a zoneId', function () { const response = { body: { @@ -2133,6 +2500,87 @@ describe('The Criteo bidding adapter', function () { expect(bids[0].height).to.equal(90); }); + it('should properly parse a bid response with FLEDGE auction configs', function () { + const response = { + body: { + ext: { + igbid: [{ + impid: 'test-bidId', + igbuyer: [{ + origin: 'https://first-buyer-domain.com', + buyerdata: { + foo: 'bar', + }, + }, { + origin: 'https://second-buyer-domain.com', + buyerdata: { + foo: 'baz', + }, + }] + }], + seller: 'https://seller-domain.com', + sellerTimeout: 500, + sellerSignals: { + foo: 'bar', + }, + }, + }, + }; + const bidderRequest = { + ortb2: { + source: { + tid: 'abc' + } + } + }; + const bidRequests = [ + { + bidId: 'test-bidId', + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + bidFloor: 1, + bidFloorCur: 'EUR' + } + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const interpretedResponse = spec.interpretResponse(response, request); + expect(interpretedResponse).to.have.property('bids'); + expect(interpretedResponse).to.have.property('fledgeAuctionConfigs'); + expect(interpretedResponse.bids).to.have.lengthOf(0); + expect(interpretedResponse.fledgeAuctionConfigs).to.have.lengthOf(1); + expect(interpretedResponse.fledgeAuctionConfigs[0]).to.deep.equal({ + bidId: 'test-bidId', + config: { + auctionSignals: {}, + decisionLogicUrl: 'https://grid-mercury.criteo.com/fledge/decision', + interestGroupBuyers: ['https://first-buyer-domain.com', 'https://second-buyer-domain.com'], + perBuyerSignals: { + 'https://first-buyer-domain.com': { + foo: 'bar', + }, + 'https://second-buyer-domain.com': { + foo: 'baz' + }, + }, + seller: 'https://seller-domain.com', + sellerTimeout: 500, + sellerSignals: { + foo: 'bar', + floor: 1, + sellerCurrency: 'EUR', + }, + }, + }); + }); + [{ hasBidResponseLevelPafData: true, hasBidResponseBidLevelPafData: true, diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index aaf63873d93..975271738e5 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -52,17 +52,21 @@ describe('CriteoId module', function () { }); const storageTestCases = [ - { cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, - { cookie: 'bidId', localStorage: undefined, expected: 'bidId' }, - { cookie: undefined, localStorage: 'bidId', expected: 'bidId' }, - { cookie: undefined, localStorage: undefined, expected: undefined }, + { submoduleConfig: undefined, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, + { submoduleConfig: undefined, cookie: 'bidId', localStorage: undefined, expected: 'bidId' }, + { submoduleConfig: undefined, cookie: undefined, localStorage: 'bidId', expected: 'bidId' }, + { submoduleConfig: undefined, cookie: undefined, localStorage: undefined, expected: undefined }, + { submoduleConfig: { storage: { type: 'cookie' } }, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, + { submoduleConfig: { storage: { type: 'cookie' } }, cookie: undefined, localStorage: 'bidId2', expected: undefined }, + { submoduleConfig: { storage: { type: 'html5' } }, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId2' }, + { submoduleConfig: { storage: { type: 'html5' } }, cookie: 'bidId', localStorage: undefined, expected: undefined }, ] - storageTestCases.forEach(testCase => it('getId() should return the bidId when it exists in local storages', function () { + storageTestCases.forEach(testCase => it('getId() should return the user id depending on the storage type enabled and the data available', function () { getCookieStub.withArgs('cto_bidid').returns(testCase.cookie); getLocalStorageStub.withArgs('cto_bidid').returns(testCase.localStorage); - const result = criteoIdSubmodule.getId(); + const result = criteoIdSubmodule.getId(testCase.submoduleConfig); expect(result.id).to.be.deep.equal(testCase.expected ? { criteoId: testCase.expected } : undefined); expect(result.callback).to.be.a('function'); })) @@ -95,22 +99,24 @@ describe('CriteoId module', function () { }); const responses = [ - { bundle: 'bundle', bidId: 'bidId', acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: undefined, acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, - { bundle: undefined, bidId: 'bidId', acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: undefined, acwsUrl: undefined }, - { bundle: undefined, bidId: 'bidId', acwsUrl: undefined }, - { bundle: undefined, bidId: undefined, acwsUrl: 'acwsUrl' }, - { bundle: undefined, bidId: undefined, acwsUrl: ['acwsUrl', 'acwsUrl2'] }, - { bundle: undefined, bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: undefined, acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: 'bidId', acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: ['acwsUrl', 'acwsUrl2'] }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: { storage: { type: 'cookie' } }, shouldWriteCookie: true, shouldWriteLocalStorage: false, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: { storage: { type: 'html5' } }, shouldWriteCookie: false, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, ] responses.forEach(response => describe('test user sync response behavior', function () { const expirationTs = new Date(nowTimestamp + cookiesMaxAge).toString(); it('should save bidId if it exists', function () { - const result = criteoIdSubmodule.getId(); + const result = criteoIdSubmodule.getId(response.submoduleConfig); result.callback((id) => { expect(id).to.be.deep.equal(response.bidId ? { criteoId: response.bidId } : undefined); }); @@ -127,16 +133,35 @@ describe('CriteoId module', function () { expect(setCookieStub.calledWith('cto_bundle')).to.be.false; expect(setLocalStorageStub.calledWith('cto_bundle')).to.be.false; } else if (response.bundle) { - expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.true; - expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.true; - expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.true; + if (response.shouldWriteCookie) { + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.true; + } else { + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.false; + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.false; + } + + if (response.shouldWriteLocalStorage) { + expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.true; + } else { + expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.false; + } expect(triggerPixelStub.called).to.be.false; } if (response.bidId) { - expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.true; - expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.true; - expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.true; + if (response.shouldWriteCookie) { + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.true; + } else { + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.false; + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.false; + } + if (response.shouldWriteLocalStorage) { + expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.true; + } else { + expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.false; + } } else { expect(setCookieStub.calledWith('cto_bidid', '', pastDateString, null, '.com')).to.be.true; expect(setCookieStub.calledWith('cto_bidid', '', pastDateString, null, '.testdev.com')).to.be.true; diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index 88c640e38cc..623194cbee9 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -10,10 +10,12 @@ import { addBidResponseHook, currencySupportEnabled, currencyRates, - ready + responseReady } from 'modules/currency.js'; import {createBid} from '../../../src/bidfactory.js'; import CONSTANTS from '../../../src/constants.json'; +import {server} from '../../mocks/xhr.js'; +import * as events from 'src/events.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -30,12 +32,10 @@ describe('currency', function () { } beforeEach(function () { - fakeCurrencyFileServer = sinon.fakeServer.create(); - ready.reset(); + fakeCurrencyFileServer = server; }); afterEach(function () { - fakeCurrencyFileServer.restore(); setConfig({}); }); @@ -287,32 +287,56 @@ describe('currency', function () { expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('1000.000'); }); - it('uses default rates when currency file fails to load', function () { - setConfig({}); - - setConfig({ - adServerCurrency: 'USD', - defaultRates: { - USD: { - JPY: 100 + describe('when rates fail to load', () => { + let bid, addBidResponse, reject; + beforeEach(() => { + bid = makeBid({cpm: 100, currency: 'JPY', bidder: 'rubicoin'}); + addBidResponse = sinon.spy(); + reject = sinon.spy(); + }) + it('uses default rates if specified', function () { + setConfig({ + adServerCurrency: 'USD', + defaultRates: { + USD: { + JPY: 100 + } } - } - }); - - // default response is 404 - fakeCurrencyFileServer.respond(); + }); - var bid = { cpm: 100, currency: 'JPY', bidder: 'rubicon' }; - var innerBid; + // default response is 404 + addBidResponseHook(addBidResponse, 'au', bid); + fakeCurrencyFileServer.respond(); + sinon.assert.calledWith(addBidResponse, 'au', sinon.match(innerBid => { + expect(innerBid.cpm).to.equal('1.0000'); + expect(typeof innerBid.getCpmInNewCurrency).to.equal('function'); + expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('100.000'); + return true; + })); + }); - addBidResponseHook(function(adCodeId, bid) { - innerBid = bid; - }, 'elementId', bid); + it('rejects bids if no default rates are specified', () => { + setConfig({ + adServerCurrency: 'USD', + }); + addBidResponseHook(addBidResponse, 'au', bid, reject); + fakeCurrencyFileServer.respond(); + sinon.assert.notCalled(addBidResponse); + sinon.assert.calledWith(reject, CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + }); - expect(innerBid.cpm).to.equal('1.0000'); - expect(typeof innerBid.getCpmInNewCurrency).to.equal('function'); - expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('100.000'); - }); + it('attempts to load rates again on the next auction', () => { + setConfig({ + adServerCurrency: 'USD', + }); + fakeCurrencyFileServer.respond(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {}); + addBidResponseHook(addBidResponse, 'au', bid, reject); + fakeCurrencyFileServer.respond(); + sinon.assert.calledWith(addBidResponse, 'au', bid, reject); + }) + }) }); describe('currency.addBidResponseDecorator bidResponseQueue', function () { @@ -321,29 +345,26 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); - var bid = { 'cpm': 1, 'currency': 'USD' }; + const bid = { 'cpm': 1, 'currency': 'USD' }; setConfig({ 'adServerCurrency': 'JPY' }); - var marker = false; - let promiseResolved = false; + let responseAdded = false; + let isReady = false; + responseReady.promise.then(() => { isReady = true }); + addBidResponseHook(Object.assign(function() { - marker = true; - }, { - bail: function (promise) { - promise.then(() => promiseResolved = true); - } + responseAdded = true; }), 'elementId', bid); - expect(marker).to.equal(false); - setTimeout(() => { - expect(promiseResolved).to.be.false; + expect(responseAdded).to.equal(false); + expect(isReady).to.equal(false); fakeCurrencyFileServer.respond(); setTimeout(() => { - expect(marker).to.equal(true); - expect(promiseResolved).to.be.true; + expect(responseAdded).to.equal(true); + expect(isReady).to.equal(true); done(); }); }); @@ -419,6 +440,23 @@ describe('currency', function () { expect(reject.calledOnce).to.be.true; }); + it('should reject bid when rates have not loaded when the auction times out', () => { + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({'adServerCurrency': 'JPY'}); + const bid = makeBid({cpm: 1, currency: 'USD', auctionId: 'aid'}); + const noConversionBid = makeBid({cpm: 1, currency: 'JPY', auctionId: 'aid'}); + const reject = sinon.spy(); + const addBidResponse = sinon.spy(); + addBidResponseHook(addBidResponse, 'au', bid, reject); + addBidResponseHook(addBidResponse, 'au', noConversionBid, reject); + events.emit(CONSTANTS.EVENTS.AUCTION_TIMEOUT, {auctionId: 'aid'}); + fakeCurrencyFileServer.respond(); + sinon.assert.calledOnce(addBidResponse); + sinon.assert.calledWith(addBidResponse, 'au', noConversionBid, reject); + sinon.assert.calledOnce(reject); + sinon.assert.calledWith(reject, CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + }) + it('should return 1 when currency support is enabled and same currency code is requested as is set to adServerCurrency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); diff --git a/test/spec/modules/cwireBidAdapter_spec.js b/test/spec/modules/cwireBidAdapter_spec.js index 88c54212aff..8eedcdb4a07 100644 --- a/test/spec/modules/cwireBidAdapter_spec.js +++ b/test/spec/modules/cwireBidAdapter_spec.js @@ -293,4 +293,51 @@ describe('C-WIRE bid adapter', () => { expect(bids[0].ad).to.exist; }) }); + + describe('add user-syncs', function () { + it('empty user-syncs if no consent given', function () { + const userSyncs = spec.getUserSyncs({}, {}, {}, {}); + + expect(userSyncs).to.be.empty + }) + it('empty user-syncs if no syncOption enabled', function () { + let gdprConsent = { + vendorData: { + purpose: { + consents: 1 + } + }}; + const userSyncs = spec.getUserSyncs({}, {}, gdprConsent, {}); + + expect(userSyncs).to.be.empty + }) + + it('user-syncs with enabled pixel option', function () { + let gdprConsent = { + vendorData: { + purpose: { + consents: 1 + } + }}; + let synOptions = {pixelEnabled: true, iframeEnabled: true}; + const userSyncs = spec.getUserSyncs(synOptions, {}, gdprConsent, {}); + + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID'); + }) + + it('user-syncs with enabled iframe option', function () { + let gdprConsent = { + vendorData: { + purpose: { + consents: 1 + } + }}; + let synOptions = {iframeEnabled: true}; + const userSyncs = spec.getUserSyncs(synOptions, {}, gdprConsent, {}); + + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID'); + }) + }) }); diff --git a/test/spec/modules/datablocksBidAdapter_spec.js b/test/spec/modules/datablocksBidAdapter_spec.js index 537b82b0e2e..811aaab6ebb 100644 --- a/test/spec/modules/datablocksBidAdapter_spec.js +++ b/test/spec/modules/datablocksBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from '../../../modules/datablocksBidAdapter.js'; import { BotClientTests } from '../../../modules/datablocksBidAdapter.js'; import { getStorageManager } from '../../../src/storageManager.js'; +import {deepClone} from '../../../src/utils.js'; const bid = { bidId: '2dd581a2b6281d', @@ -409,7 +410,7 @@ describe('DatablocksAdapter', function() { expect(spec.isBidRequestValid(bid)).to.be.true; }); it('Should return false when host/source_id is not set', function() { - let moddedBid = Object.assign({}, bid); + let moddedBid = deepClone(bid); delete moddedBid.params.source_id; expect(spec.isBidRequestValid(moddedBid)).to.be.false; }); @@ -442,9 +443,12 @@ describe('DatablocksAdapter', function() { }); describe('buildRequests', function() { - let request = spec.buildRequests([bid, bid2, nativeBid], bidderRequest); + let request; + before(() => { + request = spec.buildRequests([bid, bid2, nativeBid], bidderRequest); + expect(request).to.exist; + }) - expect(request).to.exist; it('Returns POST method', function() { expect(request.method).to.exist; expect(request.method).to.equal('POST'); diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 89485adf28b..4c12e9fa211 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -58,8 +58,18 @@ describe('The DFP video support module', function () { }, options))); const prm = utils.parseQS(url.query); expect(prm.description_url).to.eql('example.com'); - }) - }) + }); + + it('should use a URI encoded page location as default for description_url', () => { + sandbox.stub(dep, 'ri').callsFake(() => ({page: 'https://example.com?iu=/99999999/news&cust_params=current_hour%3D12%26newscat%3Dtravel&pbjs_debug=true'})); + const url = parse(buildDfpVideoUrl(Object.assign({ + adUnit: adUnit, + bid: bid, + }, options))); + const prm = utils.parseQS(url.query); + expect(prm.description_url).to.eql('https%3A%2F%2Fexample.com%3Fiu%3D%2F99999999%2Fnews%26cust_params%3Dcurrent_hour%253D12%2526newscat%253Dtravel%26pbjs_debug%3Dtrue'); + }); + }); }) it('should make a legal request URL when given the required params', function () { diff --git a/test/spec/modules/dgkeywordRtdProvider_spec.js b/test/spec/modules/dgkeywordRtdProvider_spec.js index 754740b7a64..ff88ea0512f 100644 --- a/test/spec/modules/dgkeywordRtdProvider_spec.js +++ b/test/spec/modules/dgkeywordRtdProvider_spec.js @@ -91,6 +91,22 @@ describe('Digital Garage Keyword Module', function () { expect(dgRtd.getTargetBidderOfDgKeywords(adUnits_no_target)).an('array') .that.is.empty; }); + it('convertKeywordsToString method unit test', function () { + const keywordsTest = [ + { keywords: { param1: 'keywords1' }, result: 'param1=keywords1' }, + { keywords: { param1: 'keywords1', param2: 'keywords2' }, result: 'param1=keywords1,param2=keywords2' }, + { keywords: { p1: 'k1', p2: 'k2', p: 'k' }, result: 'p1=k1,p2=k2,p=k' }, + { keywords: { p1: 'k1', p2: 'k2', p: ['k'] }, result: 'p1=k1,p2=k2,p=k' }, + { keywords: { p1: 'k1', p2: ['k21', 'k22'], p: ['k'] }, result: 'p1=k1,p2=k21,p2=k22,p=k' }, + { keywords: { p1: ['k11', 'k12', 'k13'], p2: ['k21', 'k22'], p: ['k'] }, result: 'p1=k11,p1=k12,p1=k13,p2=k21,p2=k22,p=k' }, + { keywords: { p1: [], p2: ['', ''], p: [''] }, result: 'p1,p2,p' }, + { keywords: { p1: 1, p2: [1, 'k2'], p: '' }, result: 'p1,p2=k2,p' }, + { keywords: { p1: ['k1', 2, 'k3'], p2: [1, 2], p: 3 }, result: 'p1=k1,p1=k3,p2,p' }, + ]; + for (const test of keywordsTest) { + expect(dgRtd.convertKeywordsToString(test.keywords)).equal(test.result); + } + }) it('should have targets', function () { const adUnits_targets = [ { @@ -242,16 +258,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.an('undefined'); + expect(targets[1].params.ortb2Imp).to.be.an('undefined'); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.an('undefined'); + expect(targets[0].params.ortb2Imp).to.be.an('undefined'); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].params.ortb2Imp).to.be.an('undefined'); expect(pbjs.getBidderConfig()).to.be.deep.equal({}); @@ -275,16 +291,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.an('undefined'); + expect(targets[1].params.ortb2Imp).to.be.an('undefined'); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.an('undefined'); + expect(targets[0].params.ortb2Imp).to.be.an('undefined'); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].params.ortb2Imp).to.be.an('undefined'); expect(pbjs.getBidderConfig()).to.be.deep.equal({}); @@ -318,16 +334,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.deep.equal(SUCCESS_RESULT); + expect(targets[1].ortb2Imp.ext.data.keywords).to.be.deep.equal(dgRtd.convertKeywordsToString(SUCCESS_RESULT)); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.deep.equal(SUCCESS_RESULT); + expect(targets[0].ortb2Imp.ext.data.keywords).to.be.deep.equal(dgRtd.convertKeywordsToString(SUCCESS_RESULT)); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].ortb2Imp).to.be.an('undefined'); if (!IGNORE_SET_ORTB2) { expect(pbjs.getBidderConfig()).to.be.deep.equal({ diff --git a/test/spec/modules/discoveryBidAdapter_spec.js b/test/spec/modules/discoveryBidAdapter_spec.js index 078add73046..8957f8bbd40 100644 --- a/test/spec/modules/discoveryBidAdapter_spec.js +++ b/test/spec/modules/discoveryBidAdapter_spec.js @@ -11,12 +11,59 @@ describe('discovery:BidAdapterTests', function () { bidder: 'discovery', params: { token: 'd0f4902b616cc5c38cbe0a08676d0ed9', + siteId: 'siteId_01', + zoneId: 'zoneId_01', + publisher: '52', + position: 'left', + referrer: 'https://discovery.popin.cc', + }, + refererInfo: { + page: 'https://discovery.popin.cc', + stack: [ + 'a.com', + 'b.com' + ] }, mediaTypes: { banner: { sizes: [[300, 250]], + pos: 'left', + }, + }, + ortb2: { + user: { + ext: { + data: { + CxSegments: [] + } + } + }, + site: { + domain: 'discovery.popin.cc', + publisher: { + domain: 'discovery.popin.cc' + }, + page: 'https://discovery.popin.cc', + cat: ['IAB-19', 'IAB-20'], }, }, + ortb2Imp: { + ext: { + gpid: 'adslot_gpid', + tid: 'tid_01', + data: { + browsi: { + browsiViewability: 'NA', + }, + adserver: { + name: 'adserver_name', + adslot: 'adslot_name', + }, + keywords: ['travel', 'sport'], + pbadslot: '202309999' + } + } + }, adUnitCode: 'regular_iframe', transactionId: 'd163f9e2-7ecd-4c2c-a3bd-28ceb52a60ee', sizes: [[300, 250]], diff --git a/test/spec/modules/dmdIdSystem_spec.js b/test/spec/modules/dmdIdSystem_spec.js index 3096a8e55f5..16c32f184a3 100644 --- a/test/spec/modules/dmdIdSystem_spec.js +++ b/test/spec/modules/dmdIdSystem_spec.js @@ -60,7 +60,7 @@ describe('Dmd ID System', function () { it('Should invoke callback with response from API call', function () { const callbackSpy = sinon.spy(); - const domain = utils.getWindowLocation() + const domain = utils.getWindowLocation().href; const callback = dmdIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; @@ -73,7 +73,7 @@ describe('Dmd ID System', function () { it('Should log error if API response is not valid', function () { const callbackSpy = sinon.spy(); - const domain = utils.getWindowLocation() + const domain = utils.getWindowLocation().href; const callback = dmdIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; diff --git a/test/spec/modules/docereeAdManagerBidAdapter_spec.js b/test/spec/modules/docereeAdManagerBidAdapter_spec.js new file mode 100644 index 00000000000..26b054f4e29 --- /dev/null +++ b/test/spec/modules/docereeAdManagerBidAdapter_spec.js @@ -0,0 +1,125 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/docereeAdManagerBidAdapter.js'; +import { config } from '../../../src/config.js'; + +describe('docereeadmanager', function () { + config.setConfig({ + docereeadmanager: { + user: { + data: { + email: '', + firstname: '', + lastname: '', + mobile: '', + specialization: '', + organization: '', + hcpid: '', + dob: '', + gender: '', + city: '', + state: '', + country: '', + hashedhcpid: '', + hashedemail: '', + hashedmobile: '', + userid: '', + zipcode: '', + userconsent: '', + }, + }, + }, + }); + let bid = { + bidId: 'testing', + bidder: 'docereeadmanager', + params: { + placementId: 'DOC-19-1', + gdpr: '1', + gdprconsent: + 'CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g', + }, + }; + + describe('isBidRequestValid', function () { + it('Should return true if placementId is present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if placementId is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('isGdprConsentPresent', function () { + it('Should return true if gdpr consent is present', function () { + expect(spec.isGdprConsentPresent(bid)).to.be.true; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + serverRequest = serverRequest[0]; + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://dai.doceree.com/drs/quest'); + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: { + cpm: 3.576, + currency: 'USD', + width: 250, + height: 300, + ad: '

I am an ad

', + ttl: 30, + creativeId: 'div-1', + netRevenue: false, + bidderCode: '123', + dealId: 232, + requestId: '123', + meta: { + brandId: null, + advertiserDomains: ['https://dai.doceree.com/drs/quest'], + }, + }, + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys( + 'requestId', + 'cpm', + 'width', + 'height', + 'ad', + 'ttl', + 'netRevenue', + 'currency', + 'mediaType', + 'creativeId', + 'meta' + ); + expect(dataItem.requestId).to.equal('123'); + expect(dataItem.cpm).to.equal(3.576); + expect(dataItem.width).to.equal(250); + expect(dataItem.height).to.equal(300); + expect(dataItem.ad).to.equal('

I am an ad

'); + expect(dataItem.ttl).to.equal(30); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.creativeId).to.equal('div-1'); + expect(dataItem.meta.advertiserDomains).to.be.an('array').that.is.not + .empty; + }); + }); +}); diff --git a/test/spec/modules/docereeBidAdapter_spec.js b/test/spec/modules/docereeBidAdapter_spec.js index dadbb56b0c0..25da8b256fc 100644 --- a/test/spec/modules/docereeBidAdapter_spec.js +++ b/test/spec/modules/docereeBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from '../../../modules/docereeBidAdapter.js'; import { config } from '../../../src/config.js'; +import * as utils from 'src/utils.js'; describe('BidlabBidAdapter', function () { config.setConfig({ @@ -102,4 +103,36 @@ describe('BidlabBidAdapter', function () { expect(dataItem.meta.advertiserDomains[0]).to.equal('doceree.com') }); }) + describe('onBidWon', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon({}); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(true); + }); + }); + describe('onTimeout', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon([]); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(true); + }); + }); }); diff --git a/test/spec/modules/dxkultureBidAdapter_spec.js b/test/spec/modules/dxkultureBidAdapter_spec.js new file mode 100644 index 00000000000..d3ae8ec5124 --- /dev/null +++ b/test/spec/modules/dxkultureBidAdapter_spec.js @@ -0,0 +1,682 @@ +import {expect} from 'chai'; +import {spec, SYNC_URL} from 'modules/dxkultureBidAdapter.js'; +import {BANNER, VIDEO} from 'src/mediaTypes.js'; + +const getBannerRequest = () => { + return { + bidderCode: 'dxkulture', + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + bidderRequestId: 'bidderRequestId', + bids: [ + { + bidder: 'dxkulture', + params: { + placementId: 123456, + publisherId: 'publisherId', + bidfloor: 10, + }, + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + placementCode: 'div-gpt-dummy-placement-code', + mediaTypes: { + banner: { + sizes: [ + [ 300, 250 ], + ] + } + }, + bidId: '2e9f38ea93bb9e', + bidderRequestId: 'bidderRequestId', + } + ], + start: 1487883186070, + auctionStart: 1487883186069, + timeout: 3000 + } +}; + +const getVideoRequest = () => { + return { + bidderCode: 'dxkulture', + auctionId: 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', + bidderRequestId: '34feaad34lkj2', + bids: [{ + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe1e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 10, + } + }, { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe2e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 10, + } + }], + auctionStart: 1520001292880, + timeout: 5000, + start: 1520001292884, + doneCbCallCount: 0, + refererInfo: { + numIframes: 1, + reachedTop: true, + referer: 'test.com' + } + }; +}; + +const getBidderResponse = () => { + return { + headers: null, + body: { + id: 'bid-response', + seatbid: [ + { + bid: [ + { + id: '2e9f38ea93bb9e', + impid: '2e9f38ea93bb9e', + price: 0.18, + adm: '', + adid: '144762342', + adomain: [ + 'https://dummydomain.com' + ], + iurl: 'iurl', + cid: '109', + crid: 'creativeId', + cat: [], + w: 300, + h: 250, + ext: { + prebid: { + type: 'banner' + }, + bidder: { + appnexus: { + brand_id: 334553, + auction_id: 514667951122925701, + bidder_id: 2, + bid_ad_type: 0 + } + } + } + } + ], + seat: 'dxkulture' + } + ], + ext: { + usersync: { + sovrn: { + status: 'none', + syncs: [ + { + url: 'urlsovrn', + type: 'iframe' + } + ] + }, + appnexus: { + status: 'none', + syncs: [ + { + url: 'urlappnexus', + type: 'pixel' + } + ] + } + }, + responsetimemillis: { + appnexus: 127 + } + } + } + }; +} + +describe('dxkultureBidAdapter', function() { + let videoBidRequest; + + const VIDEO_REQUEST = { + 'bidderCode': 'dxkulture', + 'auctionId': 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', + 'bidderRequestId': '34feaad34lkj2', + 'bids': videoBidRequest, + 'auctionStart': 1520001292880, + 'timeout': 3000, + 'start': 1520001292884, + 'doneCbCallCount': 0, + 'refererInfo': { + 'numIframes': 1, + 'reachedTop': true, + 'referer': 'test.com' + } + }; + + beforeEach(function () { + videoBidRequest = { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe1e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 0 + } + }; + }); + + describe('isValidRequest', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('should accept request if placementId and publisherId are passed', function () { + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); + + it('reject requests without params', function () { + bidderRequest.bids[0].params = {}; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + + it('returns false when banner mediaType does not exist', function () { + bidderRequest.bids[0].mediaTypes = {} + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + }); + + describe('buildRequests', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('should return expected request object', function() { + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(bidRequest.url).equal('https://ads.kulture.media/pbjs?pid=publisherId&placementId=123456'); + expect(bidRequest.method).equal('POST'); + }); + }); + + context('banner validation', function () { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('returns true when banner sizes are defined', function () { + const bid = { + bidder: 'dxkulture', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); + + it('returns false when banner sizes are invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((sizes) => { + const bid = { + bidder: 'dxkulture', + mediaTypes: { + banner: { + sizes + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + }); + + context('video validation', function () { + beforeEach(function () { + // Basic Valid BidRequest + this.bid = { + bidder: 'dxkulture', + mediaTypes: { + video: { + playerSize: [[300, 50]], + context: 'instream', + mimes: ['foo', 'bar'], + protocols: [1, 2] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + }); + + it('should return true (skip validations) when e2etest = true', function () { + this.bid.params = { + e2etest: true + }; + expect(spec.isBidRequestValid(this.bid)).to.equal(true); + }); + + it('returns false when video context is not defined', function () { + delete this.bid.mediaTypes.video.context; + + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + + it('returns false when video playserSize is invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((playerSize) => { + this.bid.mediaTypes.video.playerSize = playerSize; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video mimes is invalid', function () { + const invalidMimes = [ + undefined, + 'test', + 1, + [] + ] + + invalidMimes.forEach((mimes) => { + this.bid.mediaTypes.video.mimes = mimes; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video protocols is invalid', function () { + const invalidMimes = [ + undefined, + 'test', + 1, + [] + ] + + invalidMimes.forEach((protocols) => { + this.bid.mediaTypes.video.protocols = protocols; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + }); + + describe('buildRequests', function () { + let bidderBannerRequest; + let bidRequestsWithMediaTypes; + let mockBidderRequest; + + beforeEach(function() { + bidderBannerRequest = getBannerRequest(); + + mockBidderRequest = {refererInfo: {}}; + + bidRequestsWithMediaTypes = [{ + bidder: 'dxkulture', + params: { + publisherId: 'km123', + }, + adUnitCode: '/adunit-code/test-path', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + ortb2Imp: { + ext: { + ae: 2 + } + } + }, { + bidder: 'dxkulture', + params: { + publisherId: 'km123', + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480], + placement: 1, + plcmt: 1, + } + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2' + }]; + }); + + context('when mediaType is banner', function () { + it('creates request data', function () { + let request = spec.buildRequests(bidderBannerRequest.bids, bidderBannerRequest) + + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', bidderBannerRequest.bids[0].bidId); + }); + + it('has gdpr data if applicable', function () { + const req = Object.assign({}, getBannerRequest(), { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, + } + }); + let request = spec.buildRequests(bidderBannerRequest.bids, req); + + const payload = request.data; + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + }); + + if (FEATURES.VIDEO) { + context('video', function () { + it('should create a POST request for every bid', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(requests.method).to.equal('POST'); + expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId); + }); + + it('should attach request data', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + const [width, height] = videoBidRequest.sizes; + const VERSION = '1.0.0'; + + expect(data.imp[1].video.w).to.equal(width); + expect(data.imp[1].video.h).to.equal(height); + expect(data.imp[1].bidfloor).to.equal(videoBidRequest.params.bidfloor); + expect(data.imp[1]['video']['placement']).to.equal(videoBidRequest.params.video['placement']); + expect(data.imp[1]['video']['plcmt']).to.equal(videoBidRequest.params.video['plcmt']); + expect(data.ext.prebidver).to.equal('$prebid.version$'); + expect(data.ext.adapterver).to.equal(spec.VERSION); + }); + + it('should set pubId to e2etest when bid.params.e2etest = true', function () { + bidRequestsWithMediaTypes[0].params.e2etest = true; + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(requests.method).to.equal('POST'); + expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest'); + }); + + it('should attach End 2 End test data', function () { + bidRequestsWithMediaTypes[1].params.e2etest = true; + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + expect(data.imp[1].bidfloor).to.equal(0); + expect(data.imp[1].video.w).to.equal(640); + expect(data.imp[1].video.h).to.equal(480); + }); + }); + } + }); + + describe('interpretResponse', function() { + context('when mediaType is banner', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBannerRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('have bids', function () { + let bids = spec.interpretResponse(bidderResponse, bidRequest); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', getBidderResponse().body.seatbid[0].bid[index].impid); + expect(bids[index]).to.have.property('cpm', getBidderResponse().body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', getBidderResponse().body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', getBidderResponse().body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', getBidderResponse().body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', getBidderResponse().body.seatbid[0].bid[index].crid); + expect(bids[index].meta).to.have.property('advertiserDomains'); + expect(bids[index]).to.have.property('ttl', 300); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + }); + + context('when mediaType is video', function () { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getVideoRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('should return no bids if the response "nurl" and "adm" are missing', function () { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { + seatbid: [{ + bid: [{ + price: 6.01 + }] + }] + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); + }); + + it('should return no bids if the response "price" is missing', function () { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { + seatbid: [{ + bid: [{ + adm: '' + }] + }] + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); + }); + }); + }); + + describe('user sync', function () { + it('handles no parameters', function () { + let opts = spec.getUserSyncs({}); + expect(opts).to.be.an('array').that.is.empty; + }); + it('returns non if sync is not allowed', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + + expect(opts).to.be.an('array').that.is.empty; + }); + + describe('when gdpr applies', function () { + let gdprConsent; + let gdprPixelUrl; + const consentString = 'gdpr-pixel-consent'; + const gdprApplies = '1'; + beforeEach(() => { + gdprConsent = { + consentString, + gdprApplies: true + }; + + gdprPixelUrl = `${SYNC_URL}&gdpr=${gdprApplies}&gdpr_consent=${consentString}`; + }); + + it('when there is a response, it should have the gdpr query params', () => { + let [{url}] = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [], + gdprConsent + ); + + expect(url).to.have.string(`gdpr_consent=${consentString}`); + expect(url).to.have.string(`gdpr=${gdprApplies}`); + }); + + it('should not send signals if no consent object is available', function () { + let [{url}] = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [], + ); + expect(url).to.not.have.string('gdpr_consent='); + expect(url).to.not.have.string('gdpr='); + }); + }); + + describe('when ccpa applies', function () { + let usPrivacyConsent; + let uspPixelUrl; + const privacyString = 'TEST'; + beforeEach(() => { + usPrivacyConsent = 'TEST'; + uspPixelUrl = `${SYNC_URL}&us_privacy=${privacyString}` + }); + it('should send the us privacy string, ', () => { + let [{url}] = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [], + undefined, + usPrivacyConsent + ); + expect(url).to.have.string(`us_privacy=${privacyString}`); + }); + + it('should not send signals if no consent string is available', function () { + let [{url}] = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [], + ); + expect(url).to.not.have.string('us_privacy='); + }); + }); + }); +}); diff --git a/test/spec/modules/dynamicAdBoostRtdProvider_spec.js b/test/spec/modules/dynamicAdBoostRtdProvider_spec.js new file mode 100644 index 00000000000..66c24435589 --- /dev/null +++ b/test/spec/modules/dynamicAdBoostRtdProvider_spec.js @@ -0,0 +1,77 @@ +import { subModuleObj as rtdProvider } from 'modules/dynamicAdBoostRtdProvider.js'; +import { loadExternalScript } from '../../../src/adloader.js'; +import { expect } from 'chai'; + +const configWithParams = { + params: { + keyId: 'dynamic', + adUnits: ['gpt-123'], + threshold: 1 + } +}; + +const configWithoutRequiredParams = { + params: { + keyId: '' + } +}; + +describe('dynamicAdBoost', function() { + let clock; + let sandbox; + beforeEach(function () { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(Date.now()); + }); + afterEach(function () { + sandbox.restore(); + }); + describe('init', function() { + describe('initialize without expected params', function() { + it('fails initalize when keyId is not present', function() { + expect(rtdProvider.init(configWithoutRequiredParams)).to.be.false; + }) + }) + + describe('initialize with expected params', function() { + it('successfully initialize with load script', function() { + expect(rtdProvider.init(configWithParams)).to.be.true; + clock.tick(1000); + expect(loadExternalScript.called).to.be.true; + }) + }); + }); +}) + +describe('markViewed tests', function() { + let sandbox; + const mockObserver = { + unobserve: sinon.spy() + }; + const makeElement = (id) => { + const el = document.createElement('div'); + el.setAttribute('id', id); + return el; + } + const mockEntry = { + target: makeElement('target_id') + }; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }) + + afterEach(function() { + sandbox.restore() + }) + + it('markViewed returns a function', function() { + expect(rtdProvider.markViewed(mockEntry, mockObserver)).to.be.a('function') + }); + + it('markViewed unobserves', function() { + const func = rtdProvider.markViewed(mockEntry, mockObserver); + func(); + expect(mockObserver.unobserve.calledOnce).to.be.true; + }); +}) diff --git a/test/spec/modules/edge226BidAdapter_spec.js b/test/spec/modules/edge226BidAdapter_spec.js new file mode 100644 index 00000000000..4819d8d4a4e --- /dev/null +++ b/test/spec/modules/edge226BidAdapter_spec.js @@ -0,0 +1,373 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/edge226BidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'edge226' + +describe('Edge226BidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://ssp.dauup.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 725d631cd99..f2ccdcb3024 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -187,6 +187,24 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('bidswitch with ext', function() { + const userId = { + bidswitch: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'bidswitch.net', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('medianet', function() { const userId = { medianet: {'id': 'sample_id'} @@ -202,6 +220,156 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('medianet with ext', function() { + const userId = { + medianet: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'media.net', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('magnite', function() { + const userId = { + magnite: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'rubiconproject.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('magnite with ext', function() { + const userId = { + magnite: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'rubiconproject.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('index', function() { + const userId = { + index: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.indexexchange.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('index with ext', function() { + const userId = { + index: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.indexexchange.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('openx', function () { + const userId = { + openx: { 'id': 'sample_id' } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'openx.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('openx with ext', function () { + const userId = { + openx: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'openx.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('pubmatic', function() { + const userId = { + pubmatic: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'pubmatic.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('pubmatic with ext', function() { + const userId = { + pubmatic: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'pubmatic.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('liveIntentId; getValue call and NO ext', function() { const userId = { lipb: { @@ -348,6 +516,24 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('uid2 with ext', function() { + const userId = { + uid2: {'id': 'Sample_AD_Token', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'uidapi.com', + uids: [{ + id: 'Sample_AD_Token', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('euid', function() { const userId = { euid: {'id': 'Sample_AD_Token'} @@ -460,6 +646,21 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('operaId', function() { + const userId = { + operaId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 't.adx.opera.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + it('33acrossId', function() { const userId = { '33acrossId': { diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index 1a6cfd7afe4..a381d7644a1 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -74,6 +74,53 @@ describe('E-Planning Adapter', function () { }, 'sizes': [[300, 250], [300, 600]], }; + const validBidSpaceNameWithBidFloor = { + bidder: 'eplanning', + 'bidId': BID_ID, + params: { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'sizes': [[300, 250], [300, 600]], + }; + const validBidSpaceOutstreamWithBidFloor = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'playerSize': [300, 600], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6], + 'playbackmethod': [2], + 'skip': 1 + } + }, + }; + const validBidSpaceInstreamWithBidFloor = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6], + 'playbackmethod': [2], + 'skip': 1 + } + }, + }; const validBidSpaceOutstream = { 'bidder': 'eplanning', 'bidId': BID_ID, @@ -573,6 +620,26 @@ describe('E-Planning Adapter', function () { expect(e).to.equal(SN + ':300x250,300x600'); }); + it('should return e parameter with space name attribute with value according to the adunit sizes and bidFloor', function () { + const e = spec.buildRequests([validBidSpaceNameWithBidFloor], bidderRequest).data.e; + expect(e).to.equal(SN + ':300x250,300x600|' + validBidSpaceNameWithBidFloor.getFloor().floor); + }); + + it('should return correct e parameter with support vast with one space with size outstream and bidFloor', function () { + const data = spec.buildRequests([validBidSpaceOutstreamWithBidFloor], bidderRequest).data; + expect(data.e).to.equal('video_300x600_0:300x600;1|' + validBidSpaceOutstreamWithBidFloor.getFloor().floor); + expect(data.vctx).to.equal(2); + expect(data.vv).to.equal(3); + }); + + it('should return correct e parameter with support vast with one space with size instream with bidFloor', function () { + let bidRequests = [validBidSpaceInstreamWithBidFloor]; + const data = spec.buildRequests(bidRequests, bidderRequest).data; + expect(data.e).to.equal('video_640x480_0:640x480;1|' + validBidSpaceInstreamWithBidFloor.getFloor().floor); + expect(data.vctx).to.equal(1); + expect(data.vv).to.equal(3); + }); + it('should return correct e parameter with more than one adunit', function () { const NEW_CODE = ADUNIT_CODE + '2'; const CLEAN_NEW_CODE = CLEAN_ADUNIT_CODE + '2'; diff --git a/test/spec/modules/euidIdSystem_spec.js b/test/spec/modules/euidIdSystem_spec.js index 9a016b0facd..9e4598bb5f5 100644 --- a/test/spec/modules/euidIdSystem_spec.js +++ b/test/spec/modules/euidIdSystem_spec.js @@ -1,15 +1,12 @@ -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {coreStorage, init, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; -import { euidIdSubmodule } from 'modules/euidIdSystem.js'; +import {euidIdSubmodule} from 'modules/euidIdSystem.js'; import 'modules/consentManagement.js'; import 'src/prebid.js'; -import { getGlobal } from 'src/prebidGlobal.js'; -import { server } from 'test/mocks/xhr.js'; -import { configureTimerInterceptors } from 'test/mocks/timers.js'; -import { cookieHelpers, runAuction, apiHelpers, setGdprApplies } from './uid2IdSystem_helpers.js'; +import {apiHelpers, cookieHelpers, runAuction, setGdprApplies} from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; +import {server} from 'test/mocks/xhr'; let expect = require('chai').expect; @@ -32,12 +29,14 @@ const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ( const apiUrl = 'https://prod.euid.eu/v2/token/refresh'; const headers = { 'Content-Type': 'application/json' }; const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); -const configureEuidResponse = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); const expectToken = (bid, token) => expect(bid?.userId ?? {}).to.deep.include(makeEuidIdentityContainer(token)); const expectNoIdentity = (bid) => expect(bid).to.not.haveOwnProperty('userId'); describe('EUID module', function() { - let suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; + let suiteSandbox, restoreSubtleToUndefined = false; + + const configureEuidResponse = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + before(function() { uninstallGdprEnforcement(); hook.ready(); @@ -116,7 +115,7 @@ describe('EUID module', function() { const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); configureEuidResponse(200, makeSuccessResponseBody()); config.setConfig(makePrebidConfig({euidToken})); - apiHelpers.respondAfterDelay(1); + apiHelpers.respondAfterDelay(1, server); const bid = await runAuction(); expectToken(bid, refreshedToken); }); diff --git a/test/spec/modules/experianRtdProvider_spec.js b/test/spec/modules/experianRtdProvider_spec.js new file mode 100644 index 00000000000..fd104674d70 --- /dev/null +++ b/test/spec/modules/experianRtdProvider_spec.js @@ -0,0 +1,365 @@ +import { + EXPERIAN_RTID_DATA_KEY, + EXPERIAN_RTID_EXPIRATION_KEY, + EXPERIAN_RTID_STALE_KEY, + SUBMODULE_NAME, + experianRtdObj, + experianRtdSubmodule, EXPERIAN_RTID_NO_TRACK_KEY +} from '../../../modules/experianRtdProvider.js'; +import { getStorageManager } from '../../../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../../../src/activities/modules'; +import { safeJSONParse, timestamp } from '../../../src/utils'; +import {server} from '../../mocks/xhr.js'; + +describe('Experian realtime module', () => { + const sandbox = sinon.createSandbox(); + let requests; + + const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }) + beforeEach(() => { + requests = server.requests; + storage.removeDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null) + }) + afterEach(() => { + sandbox.restore(); + }) + // Bid request config + const reqBidsConfigObj = { + adUnits: [{ + bids: [ + { bidder: 'appnexus' } + ] + }] + }; + describe('init', () => { + it('succeeds when params have accountId', () => { + const initResult = experianRtdSubmodule.init({ params: { accountId: 'ZylatYg' } }) + expect(initResult).to.be.true; + }) + + it('fails when params don\'t have accountId', () => { + const initResult = experianRtdSubmodule.init({ }) + expect(initResult).to.be.false; + }) + }) + + describe('getBidRequestData', () => { + describe('when local storage has data, isn\'t no track, isn\'t stale and isn\'t expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + it('doesn\'t request data envelope, and alters bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig) + sandbox.assert.calledWithExactly(alterBidsSpy, bidsConfig, moduleConfig) + expect(dataEnvelopeSpy.called).to.be.false; + }) + }) + + describe('when local storage has data but it is stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 50000).toISOString(), null) + }) + it('it requests data envelope and alters bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(alterBidsSpy, bidsConfig, moduleConfig) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + }) + }) + describe('when local storage has data but it is expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now - 50000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 100000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + describe('when local storage has no data envelope', () => { + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + describe('when local storage has no track and is expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now - 50000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 100000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + + describe('when local storage has no track and is stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 50000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + + describe('when local storage has no track and isn\'t expired or stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + it('doesn\'t alter bids and doesn\'t request data envelope', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + expect(dataEnvelopeSpy.called).to.be.false; + }) + }) + }) + + describe('alterBids', () => { + describe('data envelope has every bidder from config', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + + it('alters bids for the bidders in the module config', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic'] } } + experianRtdObj.alterBids(bidsConfig, moduleConfig); + expect(bidsConfig.ortb2Fragments.bidder).to.deep.equal({pubmatic: { + experianRtidKey: 'pubmatic-encryption-key-1', + experianRtidData: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + }}) + }) + }) + describe('data envelope is missing bidders from config', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + + it('alters bids for the bidders in the module config', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + experianRtdObj.alterBids(bidsConfig, moduleConfig); + expect(bidsConfig.ortb2Fragments.bidder).to.deep.equal({ + sovrn: { + experianRtidKey: 'sovrn-encryption-key-1', + experianRtidData: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + }}) + }) + }) + }) + + describe('requestDataEnvelope', () => { + it('sends request to experian rtd and stores response', () => { + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + experianRtdObj.requestDataEnvelope(moduleConfig, { gdpr: { gdprApplies: 0, consentString: 'wow' }, uspConsent: '1YYY' }) + requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + '{"staleAt":"2023-06-01T00:00:00","expiresAt":"2023-06-03T00:00:00","status":"ok","data":[{"bidder":"pubmatic","data":{"key":"pubmatic-encryption-key-1","data":"IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=="}},{"bidder":"sovrn","data":{"key":"sovrn-encryption-key-1","data":"IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=="}}]}' + ) + + expect(requests[0].url).to.equal('https://rtid.tapad.com/acc/ZylatYg/ids?gdpr=0&gdpr_consent=wow&us_privacy=1YYY') + expect(safeJSONParse(storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null))).to.deep.equal([{bidder: 'pubmatic', data: {key: 'pubmatic-encryption-key-1', data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='}}, {bidder: 'sovrn', data: {key: 'sovrn-encryption-key-1', data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='}}]) + expect(storage.getDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY)).to.equal('2023-06-01T00:00:00') + expect(storage.getDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY)).to.equal('2023-06-03T00:00:00') + }) + }) + + describe('extractConsentQueryString', () => { + describe('when userConsent is empty', () => { + it('returns undefined', () => { + expect(experianRtdObj.extractConsentQueryString({})).to.be.undefined + }) + }) + + describe('when userConsent exists', () => { + it('builds query string', () => { + expect( + experianRtdObj.extractConsentQueryString({}, { gdpr: { gdprApplies: 1, consentString: 'this-is-something' }, uspConsent: '1YYY' }) + ).to.equal('?gdpr=1&gdpr_consent=this-is-something&us_privacy=1YYY') + }) + }) + + describe('when config.ids exists', () => { + it('builds query string', () => { + expect(experianRtdObj.extractConsentQueryString({ params: { accountId: 'ZylatYg', ids: { maid: ['424', '2982'], hem: 'my-hem' } } }, { gdpr: { gdprApplies: 1, consentString: 'this-is-something' }, uspConsent: '1YYY' })) + .to.equal('?gdpr=1&gdpr_consent=this-is-something&us_privacy=1YYY&id.maid=424&id.maid=2982&id.hem=my-hem') + }) + }) + }) +}) diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 152adba9d00..cb81c6f06de 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -621,7 +621,7 @@ describe('FeedAdAdapter', function () { expect(call.url).to.equal('https://api.feedad.com/1/prebid/web/events'); expect(JSON.parse(call.requestBody)).to.deep.equal(expectedData); expect(call.method).to.equal('POST'); - expect(call.requestHeaders).to.include({'Content-Type': 'application/json;charset=utf-8'}); + expect(call.requestHeaders).to.include({'Content-Type': 'application/json'}); }) }); }); diff --git a/test/spec/modules/fledgeForGpt_spec.js b/test/spec/modules/fledgeForGpt_spec.js new file mode 100644 index 00000000000..60a8e196ae0 --- /dev/null +++ b/test/spec/modules/fledgeForGpt_spec.js @@ -0,0 +1,419 @@ +import { + expect +} from 'chai'; +import * as fledge from 'modules/fledgeForGpt.js'; +import {config} from '../../../src/config.js'; +import adapterManager from '../../../src/adapterManager.js'; +import * as utils from '../../../src/utils.js'; +import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; +import {hook} from '../../../src/hook.js'; +import 'modules/appnexusBidAdapter.js'; +import 'modules/rubiconBidAdapter.js'; +import {parseExtPrebidFledge, setImpExtAe, setResponseFledgeConfigs} from 'modules/fledgeForGpt.js'; +import * as events from 'src/events.js'; +import CONSTANTS from 'src/constants.json'; +import {getGlobal} from '../../../src/prebidGlobal.js'; + +describe('fledgeForGpt module', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + }); + describe('addComponentAuction', function () { + before(() => { + fledge.init({enabled: true}); + }); + + const fledgeAuctionConfig = { + seller: 'bidder', + mock: 'config' + }; + + describe('addComponentAuctionHook', function () { + let nextFnSpy, mockGptSlot; + beforeEach(function () { + nextFnSpy = sinon.spy(); + mockGptSlot = { + setConfig: sinon.stub(), + getAdUnitPath: () => 'mock/gpt/au' + }; + sandbox.stub(gptUtils, 'getGptSlotForAdUnitCode').callsFake(() => mockGptSlot); + }); + + it('should call next()', function () { + const request = {auctionId: 'aid', adUnitCode: 'auc'}; + fledge.addComponentAuctionHook(nextFnSpy, request, fledgeAuctionConfig); + sinon.assert.calledWith(nextFnSpy, request, fledgeAuctionConfig); + }); + + it('should collect auction configs and route them to GPT at end of auction', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); + const cf1 = {...fledgeAuctionConfig, id: 1, seller: 'b1'}; + const cf2 = {...fledgeAuctionConfig, id: 2, seller: 'b2'}; + fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au1'}, cf1); + fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au2'}, cf2); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au1'); + sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au2'); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'b1', + auctionConfig: cf1, + }] + }); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'b2', + auctionConfig: cf2, + }] + }); + }); + + it('should drop auction configs after end of auction', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + sinon.assert.notCalled(mockGptSlot.setConfig); + }); + + it('should augment auctionSignals with FPD', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); + fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au1', ortb2: {fpd: 1}, ortb2Imp: {fpd: 2}}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'bidder', + auctionConfig: { + ...fledgeAuctionConfig, + auctionSignals: { + prebid: { + ortb2: {fpd: 1}, + ortb2Imp: {fpd: 2} + } + } + }, + }] + }) + }) + + describe('floor signal', () => { + before(() => { + if (!getGlobal().convertCurrency) { + getGlobal().convertCurrency = () => null; + getGlobal().convertCurrency.mock = true; + } + }); + after(() => { + if (getGlobal().convertCurrency.mock) { + delete getGlobal().convertCurrency; + } + }); + + beforeEach(() => { + sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amount, from, to) => { + if (from === to) return amount; + if (from === 'USD' && to === 'JPY') return amount * 100; + if (from === 'JPY' && to === 'USD') return amount / 100; + throw new Error('unexpected currency conversion'); + }); + }); + + Object.entries({ + 'bids': (payload, values) => { + payload.bidsReceived = values + .map((val) => ({adUnitCode: 'au', cpm: val.amount, currency: val.cur})) + .concat([{adUnitCode: 'other', cpm: 10000, currency: 'EUR'}]) + }, + 'no bids': (payload, values) => { + payload.bidderRequests = values + .map((val) => ({bids: [{adUnitCode: 'au', getFloor: () => ({floor: val.amount, currency: val.cur})}]})) + .concat([{bids: {adUnitCode: 'other', getFloor: () => ({floor: -10000, currency: 'EUR'})}}]) + } + }).forEach(([tcase, setup]) => { + describe(`when auction has ${tcase}`, () => { + Object.entries({ + 'no currencies': { + values: [{amount: 1}, {amount: 100}, {amount: 10}, {amount: 100}], + 'bids': { + bidfloor: 100, + bidfloorcur: undefined + }, + 'no bids': { + bidfloor: 1, + bidfloorcur: undefined, + } + }, + 'only zero values': { + values: [{amount: 0, cur: 'USD'}, {amount: 0, cur: 'JPY'}], + 'bids': { + bidfloor: undefined, + bidfloorcur: undefined, + }, + 'no bids': { + bidfloor: undefined, + bidfloorcur: undefined, + } + }, + 'matching currencies': { + values: [{amount: 10, cur: 'JPY'}, {amount: 100, cur: 'JPY'}], + 'bids': { + bidfloor: 100, + bidfloorcur: 'JPY', + }, + 'no bids': { + bidfloor: 10, + bidfloorcur: 'JPY', + } + }, + 'mixed currencies': { + values: [{amount: 10, cur: 'USD'}, {amount: 10, cur: 'JPY'}], + 'bids': { + bidfloor: 10, + bidfloorcur: 'USD' + }, + 'no bids': { + bidfloor: 10, + bidfloorcur: 'JPY', + } + } + }).forEach(([t, testConfig]) => { + const values = testConfig.values; + const {bidfloor, bidfloorcur} = testConfig[tcase]; + + describe(`with ${t}`, () => { + let payload; + beforeEach(() => { + payload = {auctionId: 'aid'}; + setup(payload, values); + }); + + it('should populate bidfloor/bidfloorcur', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); + fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, payload); + sinon.assert.calledWith(mockGptSlot.setConfig, sinon.match(arg => { + return arg.componentAuction.some(au => au.auctionConfig.auctionSignals?.prebid?.bidfloor === bidfloor && au.auctionConfig.auctionSignals?.prebid?.bidfloorcur === bidfloorcur) + })) + }) + }); + }); + }) + }) + }); + }); + }); + + describe('fledgeEnabled', function () { + const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])); + + before(function () { + // navigator.runAdAuction & co may not exist, so we can't stub it normally with + // sinon.stub(navigator, 'runAdAuction') or something + Object.keys(navProps).forEach(p => { + navigator[p] = sinon.stub(); + }); + hook.ready(); + }); + + after(function () { + Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); + }); + + afterEach(function () { + config.resetConfig(); + }); + + const adUnits = [{ + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + }, + }, + 'bids': [ + { + 'bidder': 'appnexus', + }, + { + 'bidder': 'rubicon', + }, + ] + }]; + function expectFledgeFlags(...enableFlags) { + const bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() { + }, + [] + ); + + expect(bidRequests[0].bids[0].bidder).equals('appnexus'); + expect(bidRequests[0].fledgeEnabled).to.eql(enableFlags[0].enabled) + bidRequests[0].bids.forEach(bid => expect(bid.ortb2Imp.ext.ae).to.eql(enableFlags[0].ae)) + + expect(bidRequests[1].bids[0].bidder).equals('rubicon'); + expect(bidRequests[1].fledgeEnabled).to.eql(enableFlags[1].enabled) + bidRequests[1].bids.forEach(bid => expect(bid.ortb2Imp?.ext?.ae).to.eql(enableFlags[1].ae)); + } + + describe('with setBidderConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({bidderSequence: 'fixed'}); + config.setBidderConfig({ + bidders: ['appnexus'], + config: { + fledgeEnabled: true, + defaultForSlots: 1, + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: void 0, ae: void 0}); + }); + }); + + describe('with setConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({ + bidderSequence: 'fixed', + fledgeForGpt: { + enabled: true, + bidders: ['appnexus'], + defaultForSlots: 1, + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: void 0, ae: void 0}); + }); + + it('should set fledgeEnabled correctly for all bidders', function () { + config.setConfig({ + bidderSequence: 'fixed', + fledgeForGpt: { + enabled: true, + defaultForSlots: 1, + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1}); + }); + + it('should not override pub-defined ext.ae', () => { + config.setConfig({ + bidderSequence: 'fixed', + fledgeForGpt: { + enabled: true, + defaultForSlots: 1, + } + }); + Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 0}}}); + expectFledgeFlags({enabled: true, ae: 0}, {enabled: true, ae: 0}); + }) + }); + }); + + describe('ortb processors for fledge', () => { + it('imp.ext.ae should be removed if fledge is not enabled', () => { + const imp = {ext: {ae: 1}}; + setImpExtAe(imp, {}, {bidderRequest: {}}); + expect(imp.ext.ae).to.not.exist; + }); + it('imp.ext.ae should be left intact if fledge is enabled', () => { + const imp = {ext: {ae: 2}}; + setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}}); + expect(imp.ext.ae).to.equal(2); + }); + describe('parseExtPrebidFledge', () => { + function packageConfigs(configs) { + return { + ext: { + prebid: { + fledge: { + auctionconfigs: configs + } + } + } + }; + } + + function generateImpCtx(fledgeFlags) { + return Object.fromEntries(Object.entries(fledgeFlags).map(([impid, fledgeEnabled]) => [impid, {imp: {ext: {ae: fledgeEnabled}}}])); + } + + function generateCfg(impid, ...ids) { + return ids.map((id) => ({impid, config: {id}})); + } + + function extractResult(ctx) { + return Object.fromEntries( + Object.entries(ctx) + .map(([impid, ctx]) => [impid, ctx.fledgeConfigs?.map(cfg => cfg.config.id)]) + .filter(([_, val]) => val != null) + ); + } + + it('should collect fledge configs by imp', () => { + const ctx = { + impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) + }; + const resp = packageConfigs( + generateCfg('e1', 1, 2, 3) + .concat(generateCfg('e2', 4) + .concat(generateCfg('d1', 5, 6))) + ); + parseExtPrebidFledge({}, resp, ctx); + expect(extractResult(ctx.impContext)).to.eql({ + e1: [1, 2, 3], + e2: [4], + }); + }); + it('should not choke if fledge config references unknown imp', () => { + const ctx = {impContext: generateImpCtx({i: 1})}; + const resp = packageConfigs(generateCfg('unknown', 1)); + parseExtPrebidFledge({}, resp, ctx); + expect(extractResult(ctx.impContext)).to.eql({}); + }); + }); + describe('setResponseFledgeConfigs', () => { + it('should set fledgeAuctionConfigs paired with their corresponding bid id', () => { + const ctx = { + impContext: { + 1: { + bidRequest: {bidId: 'bid1'}, + fledgeConfigs: [{config: {id: 1}}, {config: {id: 2}}] + }, + 2: { + bidRequest: {bidId: 'bid2'}, + fledgeConfigs: [{config: {id: 3}}] + }, + 3: { + bidRequest: {bidId: 'bid3'} + } + } + }; + const resp = {}; + setResponseFledgeConfigs(resp, {}, ctx); + expect(resp.fledgeAuctionConfigs).to.eql([ + {bidId: 'bid1', config: {id: 1}}, + {bidId: 'bid1', config: {id: 2}}, + {bidId: 'bid2', config: {id: 3}}, + ]); + }); + it('should not set fledgeAuctionConfigs if none exist', () => { + const resp = {}; + setResponseFledgeConfigs(resp, {}, { + impContext: { + 1: { + fledgeConfigs: [] + }, + 2: {} + } + }); + expect(resp).to.eql({}); + }); + }); + }); +}); diff --git a/test/spec/modules/fledge_spec.js b/test/spec/modules/fledge_spec.js deleted file mode 100644 index a81ff05596e..00000000000 --- a/test/spec/modules/fledge_spec.js +++ /dev/null @@ -1,208 +0,0 @@ -import { - expect -} from 'chai'; -import * as fledge from 'modules/fledgeForGpt.js'; -import {config} from '../../../src/config.js'; -import adapterManager from '../../../src/adapterManager.js'; -import * as utils from '../../../src/utils.js'; -import {hook} from '../../../src/hook.js'; -import 'modules/appnexusBidAdapter.js'; -import 'modules/rubiconBidAdapter.js'; -import {parseExtPrebidFledge, setImpExtAe, setResponseFledgeConfigs} from 'modules/fledgeForGpt.js'; - -const CODE = 'sampleBidder'; -const AD_UNIT_CODE = 'mock/placement'; - -describe('fledgeForGpt module', function() { - let nextFnSpy; - fledge.init({enabled: true}) - - const bidRequest = { - adUnitCode: AD_UNIT_CODE, - bids: [{ - bidId: '1', - bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: AD_UNIT_CODE, - transactionId: 'au', - }] - }; - const fledgeAuctionConfig = { - bidId: '1', - } - - describe('addComponentAuctionHook', function() { - beforeEach(function() { - nextFnSpy = sinon.spy(); - }); - - it('should call next() when a proper adUnitCode and fledgeAuctionConfig are provided', function() { - fledge.addComponentAuctionHook(nextFnSpy, bidRequest.adUnitCode, fledgeAuctionConfig); - expect(nextFnSpy.called).to.be.true; - }); - }); -}); - -describe('fledgeEnabled', function () { - const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])) - - before(function () { - // navigator.runAdAuction & co may not exist, so we can't stub it normally with - // sinon.stub(navigator, 'runAdAuction') or something - Object.keys(navProps).forEach(p => { navigator[p] = sinon.stub() }); - hook.ready(); - }); - - after(function() { - Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); - }) - - afterEach(function () { - config.resetConfig(); - }); - - it('should set fledgeEnabled correctly per bidder', function () { - config.setConfig({bidderSequence: 'fixed'}) - config.setBidderConfig({ - bidders: ['appnexus'], - config: { - fledgeEnabled: true, - } - }); - - const adUnits = [{ - 'code': '/19968336/header-bid-tag1', - 'mediaTypes': { - 'banner': { - 'sizes': [[728, 90]] - }, - }, - 'bids': [ - { - 'bidder': 'appnexus', - }, - { - 'bidder': 'rubicon', - }, - ] - }]; - - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[1].fledgeEnabled).to.be.undefined; - }); -}); - -describe('ortb processors for fledge', () => { - describe('imp.ext.ae', () => { - it('should be removed if fledge is not enabled', () => { - const imp = {ext: {ae: 1}}; - setImpExtAe(imp, {}, {bidderRequest: {}}); - expect(imp.ext.ae).to.not.exist; - }) - it('should be left intact if fledge is enabled', () => { - const imp = {ext: {ae: false}}; - setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}}); - expect(imp.ext.ae).to.equal(false); - }); - }); - describe('parseExtPrebidFledge', () => { - function packageConfigs(configs) { - return { - ext: { - prebid: { - fledge: { - auctionconfigs: configs - } - } - } - } - } - - function generateImpCtx(fledgeFlags) { - return Object.fromEntries(Object.entries(fledgeFlags).map(([impid, fledgeEnabled]) => [impid, {imp: {ext: {ae: fledgeEnabled}}}])); - } - - function generateCfg(impid, ...ids) { - return ids.map((id) => ({impid, config: {id}})); - } - - function extractResult(ctx) { - return Object.fromEntries( - Object.entries(ctx) - .map(([impid, ctx]) => [impid, ctx.fledgeConfigs?.map(cfg => cfg.config.id)]) - .filter(([_, val]) => val != null) - ); - } - - it('should collect fledge configs by imp', () => { - const ctx = { - impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) - }; - const resp = packageConfigs( - generateCfg('e1', 1, 2, 3) - .concat(generateCfg('e2', 4) - .concat(generateCfg('d1', 5, 6))) - ); - parseExtPrebidFledge({}, resp, ctx); - expect(extractResult(ctx.impContext)).to.eql({ - e1: [1, 2, 3], - e2: [4], - }); - }); - it('should not choke if fledge config references unknown imp', () => { - const ctx = {impContext: generateImpCtx({i: 1})}; - const resp = packageConfigs(generateCfg('unknown', 1)); - parseExtPrebidFledge({}, resp, ctx); - expect(extractResult(ctx.impContext)).to.eql({}); - }); - }); - describe('setResponseFledgeConfigs', () => { - it('should set fledgeAuctionConfigs paired with their corresponding bid id', () => { - const ctx = { - impContext: { - 1: { - bidRequest: {bidId: 'bid1'}, - fledgeConfigs: [{config: {id: 1}}, {config: {id: 2}}] - }, - 2: { - bidRequest: {bidId: 'bid2'}, - fledgeConfigs: [{config: {id: 3}}] - }, - 3: { - bidRequest: {bidId: 'bid3'} - } - } - }; - const resp = {}; - setResponseFledgeConfigs(resp, {}, ctx); - expect(resp.fledgeAuctionConfigs).to.eql([ - {bidId: 'bid1', config: {id: 1}}, - {bidId: 'bid1', config: {id: 2}}, - {bidId: 'bid2', config: {id: 3}}, - ]); - }); - it('should not set fledgeAuctionConfigs if none exist', () => { - const resp = {}; - setResponseFledgeConfigs(resp, {}, { - impContext: { - 1: { - fledgeConfigs: [] - }, - 2: {} - } - }); - expect(resp).to.eql({}); - }); - }); -}); diff --git a/test/spec/modules/flippBidAdapter_spec.js b/test/spec/modules/flippBidAdapter_spec.js new file mode 100644 index 00000000000..518052ad91e --- /dev/null +++ b/test/spec/modules/flippBidAdapter_spec.js @@ -0,0 +1,170 @@ +import {expect} from 'chai'; +import {spec} from 'modules/flippBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; +const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding'; +describe('flippAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'flipp', + params: { + publisherNameIdentifier: 'random', + siteId: 1234, + zoneIds: [1, 2, 3, 4], + } + }; + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let invalidBid = Object.assign({}, bid); + invalidBid.params = { siteId: 1234 } + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [{ + bidder: 'flipp', + params: { + siteId: 1234, + }, + adUnitCode: '/10000/unit_code', + sizes: [[300, 600]], + mediaTypes: {banner: {sizes: [[300, 600]]}}, + bidId: '237f4d1a293f99', + bidderRequestId: '1a857fa34c1c96', + auctionId: 'a297d1aa-7900-4ce4-a0aa-caa8d46c4af7', + transactionId: '00b2896c-2731-4f01-83e4-7a3ad5da13b6', + }]; + const bidderRequest = { + refererInfo: { + referer: 'http://example.com' + } + }; + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.method).to.equal('POST'); + }); + + it('sends bid request to ENDPOINT with query parameter', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + }); + }); + + describe('interpretResponse', function() { + it('should get correct bid response', function() { + const bidRequest = { + method: 'POST', + url: ENDPOINT, + data: { + placements: [{ + divName: 'slot', + networkId: 12345, + siteId: 12345, + adTypes: [12345], + count: 1, + prebid: { + requestId: '237f4d1a293f99', + publisherNameIdentifier: 'bid.params.publisherNameIdentifier', + height: 600, + width: 300, + }, + user: '10462725-da61-4d3a-beff-6d05239e9a6e"', + }], + url: 'http://example.com', + }, + }; + + const serverResponse = { + body: { + 'decisions': { + 'inline': [{ + 'bidCpm': 1, + 'adId': 262838368, + 'height': 600, + 'width': 300, + 'storefront': { 'flyer_id': 5435567 }, + 'prebid': { + 'requestId': '237f4d1a293f99', + 'cpm': 1.11, + 'creative': 'Returned from server', + } + }] + }, + 'location': {'city': 'Oakville'}, + }, + }; + + const expectedResponse = [ + { + bidderCode: 'flipp', + requestId: '237f4d1a293f99', + currency: 'USD', + cpm: 1.11, + netRevenue: true, + width: 300, + height: 600, + creativeId: 262838368, + ttl: 30, + ad: 'Returned from server', + } + ]; + + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result).to.have.lengthOf(1); + expect(result).to.deep.have.same.members(expectedResponse); + }); + + it('should get empty bid response when no ad is returned', function() { + const bidRequest = { + method: 'POST', + url: ENDPOINT, + data: { + placements: [{ + divName: 'slot', + networkId: 12345, + siteId: 12345, + adTypes: [12345], + count: 1, + prebid: { + requestId: '237f4d1a293f99', + publisherNameIdentifier: 'bid.params.publisherNameIdentifier', + height: 600, + width: 300, + }, + user: '10462725-da61-4d3a-beff-6d05239e9a6e"', + }], + url: 'http://example.com', + }, + }; + + const serverResponse = { + body: { + 'decisions': { + 'inline': [] + }, + 'location': {'city': 'Oakville'}, + }, + }; + + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result).to.have.lengthOf(0); + expect(result).to.deep.have.same.members([]); + }) + + it('should get empty response when bid server returns 204', function() { + expect(spec.interpretResponse({})).to.be.empty; + }); + }); +}); diff --git a/test/spec/modules/fluctBidAdapter_spec.js b/test/spec/modules/fluctBidAdapter_spec.js index d970f70ad85..ff6f8562a4e 100644 --- a/test/spec/modules/fluctBidAdapter_spec.js +++ b/test/spec/modules/fluctBidAdapter_spec.js @@ -99,6 +99,79 @@ describe('fluctAdapter', function () { expect(request.data.page).to.eql('http://example.com'); }); + it('sends no transactionId by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.transactionId).to.eql(undefined); + }); + + it('sends ortb2Imp.ext.tid as transactionId', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + tid: 'tid', + } + }, + })), bidderRequest)[0]; + expect(request.data.transactionId).to.eql('tid'); + }); + + it('sends no gpid by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.gpid).to.eql(undefined); + }); + + it('sends ortb2Imp.ext.gpid as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + gpid: 'gpid', + data: { + pbadslot: 'data-pbadslot', + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('gpid'); + }); + + it('sends ortb2Imp.ext.data.pbadslot as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + data: { + pbadslot: 'data-pbadslot', + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('data-pbadslot'); + }); + + it('sends ortb2Imp.ext.data.adserver.adslot as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + data: { + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('data-adserver-adslot'); + }); + it('includes data.user.eids = [] by default', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; expect(request.data.user.eids).to.eql([]); @@ -119,14 +192,14 @@ describe('fluctAdapter', function () { expect(request.data.regs).to.eql(undefined); }); - it('includes filtered user.eids if any exist', function () { + it('includes filtered user.eids if any exists', function () { const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { userIdAsEids: [ { source: 'foobar.com', uids: [ - { id: 'foobar-id' } + { id: 'foobar-id' }, ], }, { @@ -138,19 +211,19 @@ describe('fluctAdapter', function () { { source: 'criteo.com', uids: [ - { id: 'criteo-id' } + { id: 'criteo-id' }, ], }, { source: 'intimatemerger.com', uids: [ - { id: 'imuid' } + { id: 'imuid' }, ], }, { source: 'liveramp.com', uids: [ - { id: 'idl-env' } + { id: 'idl-env' }, ], }, ], @@ -158,36 +231,96 @@ describe('fluctAdapter', function () { ); const request = spec.buildRequests(bidRequests2, bidderRequest)[0]; expect(request.data.user.eids).to.eql([ + { + source: 'foobar.com', + uids: [ + { id: 'foobar-id' }, + ], + }, { source: 'adserver.org', uids: [ - { id: 'tdid' } + { id: 'tdid' }, ], }, { source: 'criteo.com', uids: [ - { id: 'criteo-id' } + { id: 'criteo-id' }, ], }, { source: 'intimatemerger.com', uids: [ - { id: 'imuid' } + { id: 'imuid' }, ], }, { source: 'liveramp.com', uids: [ - { id: 'idl-env' } + { id: 'idl-env' }, ], }, ]); }); + it('includes user.data if any exists', function () { + const bidderRequest2 = Object.assign({}, bidderRequest, { + ortb2: { + user: { + data: [ + { + name: 'a1mediagroup.com', + ext: { + segtax: 900, + }, + segment: [ + { id: 'seg-1' }, + { id: 'seg-2' }, + ], + }, + ], + ext: { + eids: [ + { + source: 'a1mediagroup.com', + uids: [ + { id: 'aud-1' } + ], + }, + ], + }, + }, + }, + }); + const request = spec.buildRequests(bidRequests, bidderRequest2)[0]; + expect(request.data.user).to.eql({ + data: [ + { + name: 'a1mediagroup.com', + ext: { + segtax: 900, + }, + segment: [ + { id: 'seg-1' }, + { id: 'seg-2' }, + ], + }, + ], + eids: [ + { + source: 'a1mediagroup.com', + uids: [ + { id: 'aud-1' } + ], + }, + ], + }); + }); + it('includes data.params.kv if any exists', function () { const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { params: { kv: { imsids: ['imsid1', 'imsid2'] @@ -204,7 +337,7 @@ describe('fluctAdapter', function () { it('includes data.schain if any exists', function () { // this should be done by schain.js const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { schain: { ver: '1.0', complete: 1, @@ -271,7 +404,7 @@ describe('fluctAdapter', function () { }); }); - describe('interpretResponse', function() { + describe('should interpretResponse', function() { const callBeaconSnippet = '', - 'adid': '144762342', - 'adomain': [ - 'https://dummydomain.com' - ], - 'iurl': 'iurl', - 'cid': '109', - 'crid': 'creativeId', - 'cat': [], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 334553, - 'auction_id': 514667951122925701, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - }, - { - 'id': 'bidId2', - 'impid': 'bidId2', - 'price': 0.1, - 'adm': '', - 'adid': '144762342', - 'adomain': [ - 'https://dummydomain.com' - ], - 'iurl': 'iurl', - 'cid': '109', - 'crid': 'creativeId', - 'cat': [], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 386046, - 'auction_id': 517067951122925501, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - } - ], - 'seat': 'kulturemedia' - } - ], - 'ext': { - 'usersync': { - 'sovrn': { - 'status': 'none', - 'syncs': [ - { - 'url': 'urlsovrn', - 'type': 'iframe' - } - ] - }, - 'appnexus': { - 'status': 'none', - 'syncs': [ - { - 'url': 'urlappnexus', - 'type': 'pixel' - } - ] - } - }, - 'responsetimemillis': { - 'appnexus': 127 - } - } - } -}; - -const DEFAULT_NETWORK_ID = 1; - -describe('kulturemediaBidAdapter:', function () { - let videoBidRequest; - - const VIDEO_REQUEST = { - 'bidderCode': 'kulturemedia', - 'auctionId': 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', - 'bidderRequestId': '34feaad34lkj2', - 'bids': videoBidRequest, - 'auctionStart': 1520001292880, - 'timeout': 3000, - 'start': 1520001292884, - 'doneCbCallCount': 0, - 'refererInfo': { - 'numIframes': 1, - 'reachedTop': true, - 'referer': 'test.com' - } - }; - - beforeEach(function () { - videoBidRequest = { - mediaTypes: { - video: { - context: 'instream', - playerSize: [[640, 480]], - } - }, - bidder: 'kulturemedia', - sizes: [640, 480], - bidId: '30b3efwfwe1e', - adUnitCode: 'video1', - params: { - video: { - playerWidth: 640, - playerHeight: 480, - mimes: ['video/mp4', 'application/javascript'], - protocols: [2, 5], - api: [2], - position: 1, - delivery: [2], - sid: 134, - rewarded: 1, - placement: 1, - hp: 1, - inventoryid: 123 - }, - site: { - id: 1, - page: 'https://test.com', - referrer: 'http://test.com' - }, - publisherId: 'km123' - } - }; - }); - - describe('isBidRequestValid', function () { - context('basic validation', function () { - beforeEach(function () { - // Basic Valid BidRequest - this.bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes: [[250, 300]] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - }); - - it('should accept request if placementId and publisherId are passed', function () { - expect(spec.isBidRequestValid(this.bid)).to.be.true; - }); - - it('reject requests without params', function () { - this.bid.params = {}; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - - it('returns false when banner mediaType does not exist', function () { - this.bid.mediaTypes = {} - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - }); - - context('banner validation', function () { - it('returns true when banner sizes are defined', function () { - const bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes: [[250, 300]] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - - expect(spec.isBidRequestValid(bid)).to.be.true; - }); - - it('returns false when banner sizes are invalid', function () { - const invalidSizes = [ - undefined, - '2:1', - 123, - 'test' - ]; - - invalidSizes.forEach((sizes) => { - const bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - - expect(spec.isBidRequestValid(bid)).to.be.false; - }); - }); - }); - - context('video validation', function () { - beforeEach(function () { - // Basic Valid BidRequest - this.bid = { - bidder: 'kulturemedia', - mediaTypes: { - video: { - playerSize: [[300, 50]], - context: 'instream', - mimes: ['foo', 'bar'], - protocols: [1, 2] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - }); - - it('should return true (skip validations) when e2etest = true', function () { - this.bid.params = { - e2etest: true - }; - expect(spec.isBidRequestValid(this.bid)).to.equal(true); - }); - - it('returns false when video context is not defined', function () { - delete this.bid.mediaTypes.video.context; - - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - - it('returns false when video playserSize is invalid', function () { - const invalidSizes = [ - undefined, - '2:1', - 123, - 'test' - ]; - - invalidSizes.forEach((playerSize) => { - this.bid.mediaTypes.video.playerSize = playerSize; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - }); - - it('returns false when video mimes is invalid', function () { - const invalidMimes = [ - undefined, - 'test', - 1, - [] - ] - - invalidMimes.forEach((mimes) => { - this.bid.mediaTypes.video.mimes = mimes; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }) - }); - - it('returns false when video protocols is invalid', function () { - const invalidMimes = [ - undefined, - 'test', - 1, - [] - ] - - invalidMimes.forEach((protocols) => { - this.bid.mediaTypes.video.protocols = protocols; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }) - }); - }); - }); - - describe('buildRequests', function () { - context('when mediaType is banner', function () { - it('creates request data', function () { - let request = spec.buildRequests(BANNER_REQUEST.bidRequest, BANNER_REQUEST); - - expect(request).to.exist.and.to.be.a('object'); - const payload = JSON.parse(request.data); - expect(payload.imp[0]).to.have.property('id', BANNER_REQUEST.bidRequest[0].bidId); - expect(payload.imp[1]).to.have.property('id', BANNER_REQUEST.bidRequest[1].bidId); - }); - - it('has gdpr data if applicable', function () { - const req = Object.assign({}, BANNER_REQUEST, { - gdprConsent: { - consentString: 'consentString', - gdprApplies: true, - } - }); - let request = spec.buildRequests(BANNER_REQUEST.bidRequest, req); - - const payload = JSON.parse(request.data); - expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); - expect(payload.regs.ext).to.have.property('gdpr', 1); - }); - - it('should properly forward eids parameters', function () { - const req = Object.assign({}, BANNER_REQUEST); - req.bidRequest[0].userIdAsEids = [ - { - source: 'dummy.com', - uids: [ - { - id: 'd6d0a86c-20c6-4410-a47b-5cba383a698a', - atype: 1 - } - ] - }]; - let request = spec.buildRequests(req.bidRequest, req); - - const payload = JSON.parse(request.data); - expect(payload.user.ext.eids[0].source).to.equal('dummy.com'); - expect(payload.user.ext.eids[0].uids[0].id).to.equal('d6d0a86c-20c6-4410-a47b-5cba383a698a'); - expect(payload.user.ext.eids[0].uids[0].atype).to.equal(1); - }); - }); - - context('when mediaType is video', function () { - it('should create a POST request for every bid', function () { - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - expect(requests.method).to.equal('POST'); - expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId + '&nId=' + DEFAULT_NETWORK_ID); - }); - - it('should attach request data', function () { - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - const data = JSON.parse(requests.data); - const [width, height] = videoBidRequest.sizes; - const VERSION = '1.0.0'; - expect(data.imp[0].video.w).to.equal(width); - expect(data.imp[0].video.h).to.equal(height); - expect(data.imp[0].bidfloor).to.equal(videoBidRequest.params.bidfloor); - expect(data.ext.prebidver).to.equal('$prebid.version$'); - expect(data.ext.adapterver).to.equal(spec.VERSION); - }); - - it('should set pubId to e2etest when bid.params.e2etest = true', function () { - videoBidRequest.params.e2etest = true; - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - expect(requests.method).to.equal('POST'); - expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest&nId=' + DEFAULT_NETWORK_ID); - }); - - it('should attach End 2 End test data', function () { - videoBidRequest.params.e2etest = true; - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - const data = JSON.parse(requests.data); - expect(data.imp[0].bidfloor).to.not.exist; - expect(data.imp[0].video.w).to.equal(640); - expect(data.imp[0].video.h).to.equal(480); - }); - }); - }); - - describe('interpretResponse', function () { - context('when mediaType is banner', function () { - it('have bids', function () { - let bids = spec.interpretResponse(RESPONSE, BANNER_REQUEST); - expect(bids).to.be.an('array').that.is.not.empty; - validateBidOnIndex(0); - validateBidOnIndex(1); - - function validateBidOnIndex(index) { - expect(bids[index]).to.have.property('currency', 'USD'); - expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].impid); - expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); - expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); - expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); - expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); - expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); - expect(bids[index].meta).to.have.property('advertiserDomains', RESPONSE.body.seatbid[0].bid[index].adomain); - expect(bids[index]).to.have.property('ttl', 300); - expect(bids[index]).to.have.property('netRevenue', true); - } - }); - - it('handles empty response', function () { - const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}}); - const bids = spec.interpretResponse(EMPTY_RESP, BANNER_REQUEST); - - expect(bids).to.be.empty; - }); - }); - - context('when mediaType is video', function () { - it('should return no bids if the response is not valid', function () { - const bidResponse = spec.interpretResponse({ - body: null - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return no bids if the response "nurl" and "adm" are missing', function () { - const serverResponse = { - seatbid: [{ - bid: [{ - price: 6.01 - }] - }] - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return no bids if the response "price" is missing', function () { - const serverResponse = { - seatbid: [{ - bid: [{ - adm: '' - }] - }] - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return a valid video bid response with just "adm"', function () { - const serverResponse = { - id: '123', - seatbid: [{ - bid: [{ - id: 1, - adid: 123, - impid: 456, - crid: 2, - price: 6.01, - adm: '', - adomain: [ - 'kulturemedia.com' - ], - w: 640, - h: 480, - ext: { - prebid: { - type: 'video' - }, - } - }] - }], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - let o = { - requestId: serverResponse.seatbid[0].bid[0].impid, - ad: '', - cpm: serverResponse.seatbid[0].bid[0].price, - creativeId: serverResponse.seatbid[0].bid[0].crid, - vastXml: serverResponse.seatbid[0].bid[0].adm, - width: 640, - height: 480, - mediaType: 'video', - currency: 'USD', - ttl: 300, - netRevenue: true, - meta: { - advertiserDomains: ['kulturemedia.com'] - } - }; - expect(bidResponse[0]).to.deep.equal(o); - }); - - it('should default ttl to 300', function () { - const serverResponse = { - seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - it('should not allow ttl above 3601, default to 300', function () { - videoBidRequest.params.video.ttl = 3601; - const serverResponse = { - seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - it('should not allow ttl below 1, default to 300', function () { - videoBidRequest.params.video.ttl = 0; - const serverResponse = { - seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - }); - }); - - describe('getUserSyncs', function () { - it('handles no parameters', function () { - let opts = spec.getUserSyncs({}); - expect(opts).to.be.an('array').that.is.empty; - }); - it('returns non if sync is not allowed', function () { - let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); - - expect(opts).to.be.an('array').that.is.empty; - }); - - it('iframe sync enabled should return results', function () { - let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [RESPONSE]); - - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('iframe'); - expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['sovrn'].syncs[0].url); - }); - - it('pixel sync enabled should return results', function () { - let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]); - - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('image'); - expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['appnexus'].syncs[0].url); - }); - - it('all sync enabled should return all results', function () { - let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]); - - expect(opts.length).to.equal(2); - }); - }); -}) -; diff --git a/test/spec/modules/lassoBidAdapter_spec.js b/test/spec/modules/lassoBidAdapter_spec.js index 5825927a931..3695889aca0 100644 --- a/test/spec/modules/lassoBidAdapter_spec.js +++ b/test/spec/modules/lassoBidAdapter_spec.js @@ -79,10 +79,12 @@ describe('lassoBidAdapter', function () { }); describe('buildRequests', function () { - const validBidRequests = spec.buildRequests([bid], bidderRequest); - expect(validBidRequests).to.be.an('array').that.is.not.empty; - - const bidRequest = validBidRequests[0]; + let validBidRequests, bidRequest; + before(() => { + validBidRequests = spec.buildRequests([bid], bidderRequest); + expect(validBidRequests).to.be.an('array').that.is.not.empty; + bidRequest = validBidRequests[0]; + }) it('Returns valid bidRequest', function () { expect(bidRequest).to.exist; diff --git a/test/spec/modules/liveIntentIdMinimalSystem_spec.js b/test/spec/modules/liveIntentIdMinimalSystem_spec.js index 5640a6d36cb..ed78d8f6e40 100644 --- a/test/spec/modules/liveIntentIdMinimalSystem_spec.js +++ b/test/spec/modules/liveIntentIdMinimalSystem_spec.js @@ -243,12 +243,42 @@ describe('LiveIntentMinimalId', function() { it('should decode a uid2 to a seperate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }); - expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar'}}); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode values with uid2 but no nonId', function() { const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }); - expect(result).to.eql({'uid2': {'id': 'bar'}}); + expect(result).to.eql({'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a bidswitch id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a medianet id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a magnite id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an index id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an openx id to a seperate object when present', function () { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'openx': 'bar'}, 'openx': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an pubmatic id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'pubmatic': 'bar'}, 'pubmatic': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should allow disabling nonId resolution', function() { diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index 859db423eac..9f57a5aa405 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,6 +1,6 @@ import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; import * as utils from 'src/utils.js'; -import { gdprDataHandler, uspDataHandler } from '../../../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../../src/adapterManager.js'; import { server } from 'test/mocks/xhr.js'; resetLiveIntentIdSubmodule(); liveIntentIdSubmodule.setModuleMode('standard') @@ -12,6 +12,7 @@ describe('LiveIntentId', function() { let logErrorStub; let uspConsentDataStub; let gdprConsentDataStub; + let gppConsentDataStub; let getCookieStub; let getDataFromLocalStorageStub; let imgStub; @@ -24,6 +25,7 @@ describe('LiveIntentId', function() { logErrorStub = sinon.stub(utils, 'logError'); uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); }); afterEach(function() { @@ -33,6 +35,7 @@ describe('LiveIntentId', function() { logErrorStub.restore(); uspConsentDataStub.restore(); gdprConsentDataStub.restore(); + gppConsentDataStub.restore(); resetLiveIntentIdSubmodule(); }); @@ -42,11 +45,15 @@ describe('LiveIntentId', function() { gdprApplies: true, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1, 2] + }) let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&n3pc=1&gdpr_consent=consentDataString.*/); + expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&n3pc=1&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1%2C2.*/); const response = { unifiedId: 'a_unified_id', segments: [123, 234] @@ -65,9 +72,13 @@ describe('LiveIntentId', function() { gdprApplies: true, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1] + }) liveIntentIdSubmodule.getId(defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString.*/); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString&gpp_s=gppConsentDataString&gpp_as=1.*/); done(); }, 200); }); @@ -123,9 +134,13 @@ describe('LiveIntentId', function() { gdprApplies: false, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1] + }) liveIntentIdSubmodule.decode({}, defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*/); + expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1.*/); done(); }, 200); }); @@ -355,22 +370,42 @@ describe('LiveIntentId', function() { it('should decode a uid2 to a seperate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }); - expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar'}}); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode values with uid2 but no nonId', function() { + const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }); + expect(result).to.eql({'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a bidswitch id to a seperate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }); - expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar'}}); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a medianet id to a seperate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }); - expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar'}}); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode values with uid2 but no nonId', function() { - const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }); - expect(result).to.eql({'uid2': {'id': 'bar'}}); + it('should decode a magnite id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an index id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an openx id to a seperate object when present', function () { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'openx': 'bar'}, 'openx': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an pubmatic id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'pubmatic': 'bar'}, 'pubmatic': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should allow disabling nonId resolution', function() { diff --git a/test/spec/modules/lm_kiviadsBidAdapter_spec.js b/test/spec/modules/lm_kiviadsBidAdapter_spec.js new file mode 100644 index 00000000000..68ac73289cd --- /dev/null +++ b/test/spec/modules/lm_kiviadsBidAdapter_spec.js @@ -0,0 +1,455 @@ +import {expect} from 'chai'; +import {config} from 'src/config.js'; +import {spec, getBidFloor} from 'modules/lm_kiviadsBidAdapter.js'; +import {deepClone} from 'src/utils'; + +const ENDPOINT = 'https://pbjs.kiviads.live'; + +const defaultRequest = { + adUnitCode: 'test', + bidId: '1', + requestId: 'qwerty', + ortb2: { + source: { + tid: 'auctionId' + } + }, + ortb2Imp: { + ext: { + tid: 'tr1', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 200] + ] + } + }, + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + }, + bidRequestsCount: 1 +}; + +const defaultRequestVideo = deepClone(defaultRequest); +defaultRequestVideo.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } +}; +describe('lm_kiviadsBidAdapter', () => { + describe('isBidRequestValid', function () { + it('should return false when request params is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required env param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.env; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required pid param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.pid; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when video.playerSize is missing', function () { + const invalidRequest = deepClone(defaultRequestVideo); + delete invalidRequest.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + beforeEach(function () { + config.resetConfig(); + }); + + it('should send request with correct structure', function () { + const request = spec.buildRequests([defaultRequest], {}); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT + '/bid'); + expect(request.options).to.have.property('contentType').and.to.equal('application/json'); + expect(request).to.have.property('data'); + }); + + it('should build basic request structure', function () { + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); + expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.ortb2.source.tid); + expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); + expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); + expect(request).to.have.property('bc').and.to.equal(1); + expect(request).to.have.property('floor').and.to.equal(null); + expect(request).to.have.property('banner').and.to.deep.equal({sizes: [[300, 250], [300, 200]]}); + expect(request).to.have.property('gdprApplies').and.to.equal(0); + expect(request).to.have.property('consentString').and.to.equal(''); + expect(request).to.have.property('userEids').and.to.deep.equal([]); + expect(request).to.have.property('usPrivacy').and.to.equal(''); + expect(request).to.have.property('coppa').and.to.equal(0); + expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); + expect(request).to.have.property('ext').and.to.deep.equal({}); + expect(request).to.have.property('env').and.to.deep.equal({ + env: 'lm_kiviads', + pid: '40' + }); + expect(request).to.have.property('device').and.to.deep.equal({ + ua: navigator.userAgent, + lang: navigator.language + }); + }); + + it('should build request with schain', function () { + const schainRequest = deepClone(defaultRequest); + schainRequest.schain = { + validation: 'strict', + config: { + ver: '1.0' + } + }; + const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; + expect(request).to.have.property('schain').and.to.deep.equal({ + validation: 'strict', + config: { + ver: '1.0' + } + }); + }); + + it('should build request with location', function () { + const bidderRequest = { + refererInfo: { + page: 'page', + location: 'location', + domain: 'domain', + ref: 'ref', + isAmp: false + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('location'); + const location = request.location; + expect(location).to.have.property('page').and.to.equal('page'); + expect(location).to.have.property('location').and.to.equal('location'); + expect(location).to.have.property('domain').and.to.equal('domain'); + expect(location).to.have.property('ref').and.to.equal('ref'); + expect(location).to.have.property('isAmp').and.to.equal(false); + }); + + it('should build request with ortb2 info', function () { + const ortb2Request = deepClone(defaultRequest); + ortb2Request.ortb2 = { + site: { + name: 'name' + } + }; + const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; + expect(request).to.have.property('ortb2').and.to.deep.equal({ + site: { + name: 'name' + } + }); + }); + + it('should build request with ortb2Imp info', function () { + const ortb2ImpRequest = deepClone(defaultRequest); + ortb2ImpRequest.ortb2Imp = { + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }; + const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; + expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }); + }); + + it('should build request with valid bidfloor', function () { + const bfRequest = deepClone(defaultRequest); + bfRequest.getFloor = () => ({floor: 5, currency: 'USD'}); + const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; + expect(request).to.have.property('floor').and.to.equal(5); + }); + + it('should build request with gdpr consent data if applies', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'qwerty' + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('gdprApplies').and.equals(1); + expect(request).to.have.property('consentString').and.equals('qwerty'); + }); + + it('should build request with usp consent data if applies', function () { + const bidderRequest = { + uspConsent: '1YA-' + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('usPrivacy').and.equals('1YA-'); + }); + + it('should build request with coppa 1', function () { + config.setConfig({ + coppa: true + }); + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('coppa').and.equals(1); + }); + + it('should build request with extended ids', function () { + const idRequest = deepClone(defaultRequest); + idRequest.userIdAsEids = [ + {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} + ]; + const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; + expect(request).to.have.property('userEids').and.deep.equal(idRequest.userIdAsEids); + }); + + it('should build request with video', function () { + const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; + expect(request).to.have.property('video').and.to.deep.equal({ + playerSize: [640, 480], + context: 'instream', + skipppable: true + }); + expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); + }); + }); + + describe('interpretResponse', function () { + it('should return empty bids', function () { + const serverResponse = { + body: { + data: null + } + }; + + const invalidResponse = spec.interpretResponse(serverResponse, {}); + expect(invalidResponse).to.be.an('array').that.is.empty; + }); + + it('should interpret valid response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + meta: { + advertiserDomains: ['lm_kiviads'] + }, + ext: { + pixels: [ + ['iframe', 'surl1'], + ['image', 'surl2'], + ] + } + }] + } + }; + + const validResponse = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequest}); + const bid = validResponse[0]; + expect(validResponse).to.be.an('array').that.is.not.empty; + expect(bid.requestId).to.equal('qwerty'); + expect(bid.cpm).to.equal(1); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ttl).to.equal(600); + expect(bid.meta).to.deep.equal({advertiserDomains: ['lm_kiviads']}); + }); + + it('should interpret valid banner response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + mediaType: 'banner', + creativeId: 'xe-demo-banner', + ad: 'ad', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequest}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('banner'); + expect(bid.creativeId).to.equal('xe-demo-banner'); + expect(bid.ad).to.equal('ad'); + }); + + it('should interpret valid video response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 600, + height: 480, + ttl: 600, + mediaType: 'video', + creativeId: 'xe-demo-video', + ad: 'vast-xml', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequestVideo}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('video'); + expect(bid.creativeId).to.equal('xe-demo-video'); + expect(bid.ad).to.equal('vast-xml'); + }); + }); + + describe('getUserSyncs', function () { + it('shoukd handle no params', function () { + const opts = spec.getUserSyncs({}, []); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should return empty if sync is not allowed', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should allow iframe sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync and parse consent params', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }], { + gdprApplies: 1, + consentString: '1YA-' + }); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); + }); + }); + + describe('getBidFloor', function () { + it('should return null when getFloor is not a function', () => { + const bid = {getFloor: 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when getFloor doesnt return an object', () => { + const bid = {getFloor: () => 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when floor is not a number', () => { + const bid = { + getFloor: () => ({floor: 'string', currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when currency is not USD', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'EUR'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return floor value when everything is correct', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.equal(5); + }); + }); +}) diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index ae63f19f46b..0864a976d7d 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -550,7 +550,7 @@ describe('magnite analytics adapter', function () { expect(server.requests.length).to.equal(1); let request = server.requests[0]; - expect(request.url).to.equal('//localhost:9999/event'); + expect(request.url).to.match(/\/\/localhost:9999\/event/); let message = JSON.parse(request.requestBody); @@ -724,7 +724,7 @@ describe('magnite analytics adapter', function () { expect(server.requests.length).to.equal(1); let request = server.requests[0]; - expect(request.url).to.equal('//localhost:9999/event'); + expect(request.url).to.match(/\/\/localhost:9999\/event/); let message = JSON.parse(request.requestBody); @@ -1492,6 +1492,40 @@ describe('magnite analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); + describe('when eventDispatcher is present', () => { + beforeEach(() => { + window.pbjs = window.pbjs || {}; + pbjs.rp = pbjs.rp || {}; + pbjs.rp.eventDispatcher = pbjs.rp.eventDispatcher || document.createElement('fakeElem'); + }); + + afterEach(() => { + delete pbjs.rp.eventDispatcher; + delete pbjs.rp; + }); + + it('should dispatch beforeSendingMagniteAnalytics if possible', () => { + pbjs.rp.eventDispatcher.addEventListener('beforeSendingMagniteAnalytics', (data) => { + data.detail.test = 'testData'; + }); + + performStandardAuction(); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + + expect(request.url).to.equal('http://localhost:9999/event'); + + let message = JSON.parse(request.requestBody); + + const AnalyticsMessageWithCustomData = { + ...ANALYTICS_MESSAGE, + test: 'testData' + } + expect(message).to.deep.equal(AnalyticsMessageWithCustomData); + }); + }) + describe('when handling bid caching', () => { let auctionInits, bidRequests, bidResponses, bidsWon; beforeEach(function () { diff --git a/test/spec/modules/mediabramaBidAdapter_spec.js b/test/spec/modules/mediabramaBidAdapter_spec.js new file mode 100644 index 00000000000..d7341e02f17 --- /dev/null +++ b/test/spec/modules/mediabramaBidAdapter_spec.js @@ -0,0 +1,256 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/mediabramaBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; +import * as utils from '../../../src/utils.js'; + +describe('MediaBramaBidAdapter', function () { + const bid = { + bidId: '23dc19818e5293', + bidder: 'mediabrama', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 24428, + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://prebid.mediabrama.com/pbjs'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'adFormat', 'sizes', 'schain', 'bidfloor'); + expect(placement.placementId).to.equal(24428); + expect(placement.bidId).to.equal('23dc19818e5293'); + expect(placement.adFormat).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23dc19818e5293', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: {} + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23dc19818e5293'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23dc19818e5293', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function () { + it('should do nothing on getUserSyncs', function () { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://prebid.mediabrama.com/sync/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + }); + + describe('on bidWon', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('should replace nurl for banner', function () { + const nurl = 'nurl/?ap=${' + 'AUCTION_PRICE}'; + const bid = { + 'bidderCode': 'mediabrama', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '5691dd18ba6ab6', + 'requestId': '23dc19818e5293', + 'transactionId': '948c716b-bf64-4303-bcf4-395c2f6a9770', + 'auctionId': 'a6b7c61f-15a9-481b-8f64-e859787e9c07', + 'mediaType': 'banner', + 'source': 'client', + 'ad': "
\n", + 'cpm': 0.61, + 'nurl': nurl, + 'creativeId': 'test', + 'currency': 'USD', + 'dealId': '', + 'meta': { + 'advertiserDomains': [], + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [ + { + 'name': 'mediabrama' + } + ] + } + }, + 'netRevenue': true, + 'ttl': 185, + 'metrics': {}, + 'adapterCode': 'mediabrama', + 'originalCpm': 0.61, + 'originalCurrency': 'USD', + 'responseTimestamp': 1668162732297, + 'requestTimestamp': 1668162732292, + 'bidder': 'mediabrama', + 'adUnitCode': 'div-prebid', + 'timeToRespond': 5, + 'pbLg': '0.50', + 'pbMg': '0.60', + 'pbHg': '0.61', + 'pbAg': '0.61', + 'pbDg': '0.61', + 'pbCg': '', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'mediabrama', + 'hb_adid': '5691dd18ba6ab6', + 'hb_pb': '0.61', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': '' + }, + 'status': 'rendered', + 'params': [ + { + 'placementId': 24428 + } + ] + }; + spec.onBidWon(bid); + expect(bid.nurl).to.deep.equal('nurl/?ap=0.61'); + }); + }); +}); diff --git a/test/spec/modules/mediafilterRtdProvider_spec.js b/test/spec/modules/mediafilterRtdProvider_spec.js new file mode 100644 index 00000000000..3395c7be691 --- /dev/null +++ b/test/spec/modules/mediafilterRtdProvider_spec.js @@ -0,0 +1,147 @@ +import * as utils from '../../../src/utils.js'; +import * as hook from '../../../src/hook.js' +import * as events from '../../../src/events.js'; +import CONSTANTS from '../../../src/constants.json'; + +import { + MediaFilter, + MEDIAFILTER_EVENT_TYPE, + MEDIAFILTER_BASE_URL +} from '../../../modules/mediafilterRtdProvider.js'; + +describe('The Media Filter RTD module', function () { + describe('register()', function() { + let submoduleSpy, generateInitHandlerSpy; + + beforeEach(function () { + submoduleSpy = sinon.spy(hook, 'submodule'); + generateInitHandlerSpy = sinon.spy(MediaFilter, 'generateInitHandler'); + }); + + afterEach(function () { + submoduleSpy.restore(); + generateInitHandlerSpy.restore(); + }); + + it('should register and call the submodule function(s)', function () { + MediaFilter.register(); + + expect(submoduleSpy.calledOnceWithExactly('realTimeData', sinon.match.object)).to.be.true; + expect(submoduleSpy.called).to.be.true; + expect(generateInitHandlerSpy.called).to.be.true; + }); + }); + + describe('setup()', function() { + let setupEventListenerSpy, setupScriptSpy; + + beforeEach(function() { + setupEventListenerSpy = sinon.spy(MediaFilter, 'setupEventListener'); + setupScriptSpy = sinon.spy(MediaFilter, 'setupScript'); + }); + + afterEach(function() { + setupEventListenerSpy.restore(); + setupScriptSpy.restore(); + }); + + it('should call setupEventListener and setupScript function(s)', function() { + MediaFilter.setup({ configurationHash: 'abc123' }); + + expect(setupEventListenerSpy.called).to.be.true; + expect(setupScriptSpy.called).to.be.true; + }); + }); + + describe('setupEventListener()', function() { + let setupEventListenerSpy, addEventListenerSpy; + + beforeEach(function() { + setupEventListenerSpy = sinon.spy(MediaFilter, 'setupEventListener'); + addEventListenerSpy = sinon.spy(window, 'addEventListener'); + }); + + afterEach(function() { + setupEventListenerSpy.restore(); + addEventListenerSpy.restore(); + }); + + it('should call addEventListener function(s)', function() { + MediaFilter.setupEventListener(); + expect(addEventListenerSpy.called).to.be.true; + expect(addEventListenerSpy.calledWith('message', sinon.match.func)).to.be.true; + }); + }); + + describe('generateInitHandler()', function() { + let generateInitHandlerSpy, setupMock, logErrorSpy; + + beforeEach(function() { + generateInitHandlerSpy = sinon.spy(MediaFilter, 'generateInitHandler'); + setupMock = sinon.stub(MediaFilter, 'setup').throws(new Error('Mocked error!')); + logErrorSpy = sinon.spy(utils, 'logError'); + }); + + afterEach(function() { + generateInitHandlerSpy.restore(); + setupMock.restore(); + logErrorSpy.restore(); + }); + + it('should handle errors in the catch block when setup throws an error', function() { + const initHandler = MediaFilter.generateInitHandler(); + initHandler({}); + + expect(logErrorSpy.calledWith('Error in initialization: Mocked error!')).to.be.true; + }); + }); + + describe('generateEventHandler()', function() { + let generateEventHandlerSpy, eventsEmitSpy; + + beforeEach(function() { + generateEventHandlerSpy = sinon.spy(MediaFilter, 'generateEventHandler'); + eventsEmitSpy = sinon.spy(events, 'emit'); + }); + + afterEach(function() { + generateEventHandlerSpy.restore(); + eventsEmitSpy.restore(); + }); + + it('should emit a billable event when the event type matches', function() { + const configurationHash = 'abc123'; + const eventHandler = MediaFilter.generateEventHandler(configurationHash); + + const mockEvent = { + data: { + type: MEDIAFILTER_EVENT_TYPE.concat('.', configurationHash) + } + }; + + eventHandler(mockEvent); + + expect(eventsEmitSpy.calledWith(CONSTANTS.EVENTS.BILLABLE_EVENT, { + 'billingId': sinon.match.string, + 'configurationHash': configurationHash, + 'type': 'impression', + 'vendor': 'mediafilter', + })).to.be.true; + }); + + it('should not emit a billable event when the event type does not match', function() { + const configurationHash = 'abc123'; + const eventHandler = MediaFilter.generateEventHandler(configurationHash); + + const mockEvent = { + data: { + type: 'differentEventType' + } + }; + + eventHandler(mockEvent); + + expect(eventsEmitSpy.called).to.be.false; + }); + }); +}); diff --git a/test/spec/modules/mediagoBidAdapter_spec.js b/test/spec/modules/mediagoBidAdapter_spec.js index e77af544429..5eb362893e3 100644 --- a/test/spec/modules/mediagoBidAdapter_spec.js +++ b/test/spec/modules/mediagoBidAdapter_spec.js @@ -11,11 +11,40 @@ describe('mediago:BidAdapterTests', function () { bidder: 'mediago', params: { token: '85a6b01e41ac36d49744fad726e3655d', + siteId: 'siteId_01', + zoneId: 'zoneId_01', + publisher: '52', + position: 'left', + referrer: 'https://trace.mediago.io', bidfloor: 0.01, + ortb2Imp: { + ext: { + gpid: 'adslot_gpid', + tid: 'tid_01', + data: { + browsi: { + browsiViewability: 'NA', + }, + adserver: { + name: 'adserver_name', + adslot: 'adslot_name', + }, + }, + }, + }, }, mediaTypes: { banner: { sizes: [[300, 250]], + pos: 'left', + }, + }, + ortb2: { + user: { + ext: { + data: { + }, + }, }, }, adUnitCode: 'regular_iframe', diff --git a/test/spec/modules/medianetBidAdapter_spec.js b/test/spec/modules/medianetBidAdapter_spec.js index bb90eded230..4a221e97444 100644 --- a/test/spec/modules/medianetBidAdapter_spec.js +++ b/test/spec/modules/medianetBidAdapter_spec.js @@ -915,24 +915,24 @@ let VALID_BID_REQUEST = [{ cid: '8CUV090' } }, - VALID_PARAMS_AAX = { - bidder: 'aax', + VALID_PARAMS_TS = { + bidder: 'trustedstack', params: { - cid: 'AAXG123' + cid: 'TS012345' } }, PARAMS_MISSING = { bidder: 'medianet', }, - PARAMS_MISSING_AAX = { - bidder: 'aax', + PARAMS_MISSING_TS = { + bidder: 'trustedstack', }, PARAMS_WITHOUT_CID = { bidder: 'medianet', params: {} }, - PARAMS_WITHOUT_CID_AAX = { - bidder: 'aax', + PARAMS_WITHOUT_CID_TS = { + bidder: 'trustedstack', params: {} }, PARAMS_WITH_INTEGER_CID = { @@ -941,8 +941,8 @@ let VALID_BID_REQUEST = [{ cid: 8867587 } }, - PARAMS_WITH_INTEGER_CID_AAX = { - bidder: 'aax', + PARAMS_WITH_INTEGER_CID_TS = { + bidder: 'trustedstack', params: { cid: 8867587 } @@ -953,8 +953,8 @@ let VALID_BID_REQUEST = [{ cid: '' } }, - PARAMS_WITH_EMPTY_CID_AAX = { - bidder: 'aax', + PARAMS_WITH_EMPTY_CID_TS = { + bidder: 'trustedstack', params: { cid: '' } @@ -1311,6 +1311,118 @@ let VALID_BID_REQUEST = [{ } }], 'tmax': 3000, + }, + VALID_BIDDER_REQUEST_WITH_GPP_IN_ORTB2 = { + ortb2: { + regs: { + gpp: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + gpp_sid: [5, 7] + } + }, + 'timeout': 3000, + refererInfo: { + referer: 'http://media.net/prebidtest', + stack: ['http://media.net/prebidtest'], + page: 'http://media.net/page', + domain: 'media.net', + topmostLocation: 'http://media.net/topmost', + reachedTop: true + } + }, + VALID_PAYLOAD_FOR_GPP_ORTB2 = { + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'topMostLocation': 'http://media.net/topmost', + 'isTop': true + }, + 'ext': { + 'customer_id': 'customer_id', + 'prebid_version': $$PREBID_GLOBAL$$.version, + 'gdpr_applies': false, + 'usp_applies': false, + 'coppa_applies': false, + 'screen': { + 'w': 1000, + 'h': 1000 + } + }, + 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'imp': [{ + 'id': '28f8f8130a583e', + 'transactionId': '277b631f-92f5-4844-8b19-ea13c095d3f1', + ortb2Imp: VALID_BID_REQUEST[0].ortb2Imp, + 'ext': { + 'dfp_id': 'div-gpt-ad-1460505748561-0', + 'visibility': 1, + 'viewability': 1, + 'coordinates': { + 'top_left': { + x: 50, + y: 50 + }, + 'bottom_right': { + x: 100, + y: 100 + } + }, + 'display_count': 1 + }, + 'banner': [{ + 'w': 300, + 'h': 250 + }], + 'all': { + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'isTop': true + } + } + }, { + 'id': '3f97ca71b1e5c2', + 'transactionId': 'c52a5c62-3c2b-4b90-9ff8-ec1487754822', + ortb2Imp: VALID_BID_REQUEST[1].ortb2Imp, + 'ext': { + 'dfp_id': 'div-gpt-ad-1460505748561-123', + 'visibility': 1, + 'viewability': 1, + 'coordinates': { + 'top_left': { + x: 50, + y: 50 + }, + 'bottom_right': { + x: 100, + y: 100 + } + }, + 'display_count': 1 + }, + 'banner': [{ + 'w': 300, + 'h': 251 + }], + 'all': { + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'isTop': true + } + } + }], + 'ortb2': { + 'regs': { + 'gpp': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'gpp_sid': [5, 7], + } + }, + 'tmax': config.getConfig('bidderTimeout') }; describe('Media.net bid adapter', function () { let sandbox; @@ -1393,6 +1505,11 @@ describe('Media.net bid adapter', function () { expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_FOR_GDPR); }); + it('should have gpp params in ortb2', function () { + let bidReq = spec.buildRequests(VALID_BID_REQUEST, VALID_BIDDER_REQUEST_WITH_GPP_IN_ORTB2); + expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_FOR_GPP_ORTB2); + }); + it('should parse params for native request', function () { let bidReq = spec.buildRequests(VALID_NATIVE_BID_REQUEST, VALID_AUCTIONDATA); expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_NATIVE); @@ -1666,34 +1783,34 @@ describe('Media.net bid adapter', function () { }); }); - describe('isBidRequestValid aax', function () { + describe('isBidRequestValid trustedstack', function () { it('should accept valid bid params', function () { - let isValid = spec.isBidRequestValid(VALID_PARAMS_AAX); + let isValid = spec.isBidRequestValid(VALID_PARAMS_TS); expect(isValid).to.equal(true); }); it('should reject bid if cid is not present', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITHOUT_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITHOUT_CID_TS); expect(isValid).to.equal(false); }); it('should reject bid if cid is not a string', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITH_INTEGER_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITH_INTEGER_CID_TS); expect(isValid).to.equal(false); }); it('should reject bid if cid is a empty string', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITH_EMPTY_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITH_EMPTY_CID_TS); expect(isValid).to.equal(false); }); it('should have missing params', function () { - let isValid = spec.isBidRequestValid(PARAMS_MISSING_AAX); + let isValid = spec.isBidRequestValid(PARAMS_MISSING_TS); expect(isValid).to.equal(false); }); }); - describe('interpretResponse aax', function () { + describe('interpretResponse trustedstack', function () { it('should not push response if no-bid', function () { let validBids = []; let bids = spec.interpretResponse(SERVER_RESPONSE_NOBID, []); diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index 4e131024d84..d7984c05967 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -1,5 +1,6 @@ import {expect} from 'chai'; import {spec} from 'modules/mediasquareBidAdapter.js'; +import { server } from 'test/mocks/xhr.js'; describe('MediaSquare bid adapter tests', function () { var DEFAULT_PARAMS = [{ @@ -99,6 +100,8 @@ describe('MediaSquare bid adapter tests', function () { 'bid_id': 'aaaa1234', 'adomain': ['test.com'], 'context': 'instream', + 'increment': 1.0, + 'ova': 'cleared', }], }}; @@ -166,7 +169,10 @@ describe('MediaSquare bid adapter tests', function () { expect(bid.mediasquare.bidder).to.equal('msqClassic'); expect(bid.mediasquare.context).to.exist; expect(bid.mediasquare.context).to.equal('instream'); + expect(bid.mediasquare.increment).to.exist; + expect(bid.mediasquare.increment).to.equal(1.0); expect(bid.mediasquare.code).to.equal([DEFAULT_PARAMS[0].params.owner, DEFAULT_PARAMS[0].params.code].join('/')); + expect(bid.mediasquare.ova).to.exist.and.to.equal('cleared'); expect(bid.meta).to.exist; expect(bid.meta.advertiserDomains).to.exist; expect(bid.meta.advertiserDomains).to.have.lengthOf(1); @@ -205,6 +211,11 @@ describe('MediaSquare bid adapter tests', function () { const response = spec.interpretResponse(BID_RESPONSE, request); const won = spec.onBidWon(response[0]); expect(won).to.equal(true); + expect(server.requests.length).to.equal(1); + let message = JSON.parse(server.requests[0].requestBody); + expect(message).to.have.property('increment').exist; + expect(message).to.have.property('increment').and.to.equal('1'); + expect(message).to.have.property('ova').and.to.equal('cleared'); }); it('Verifies user sync without cookie in bid response', function () { var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); diff --git a/test/spec/modules/mgidRtdProvider_spec.js b/test/spec/modules/mgidRtdProvider_spec.js index 4f70b4d8b7c..996875649b6 100644 --- a/test/spec/modules/mgidRtdProvider_spec.js +++ b/test/spec/modules/mgidRtdProvider_spec.js @@ -1,16 +1,14 @@ import { mgidSubmodule, storage } from '../../../modules/mgidRtdProvider.js'; import {expect} from 'chai'; import * as refererDetection from '../../../src/refererDetection'; +import {server} from '../../mocks/xhr.js'; describe('Mgid RTD submodule', () => { - let server; let clock; let getRefererInfoStub; let getDataFromLocalStorageStub; beforeEach(() => { - server = sinon.fakeServer.create(); - clock = sinon.useFakeTimers(); getRefererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); @@ -22,7 +20,6 @@ describe('Mgid RTD submodule', () => { }); afterEach(() => { - server.restore(); clock.restore(); getRefererInfoStub.restore(); getDataFromLocalStorageStub.restore(); @@ -309,7 +306,6 @@ describe('Mgid RTD submodule', () => { server.requests[0].respond( 204, {'Content-Type': 'application/json'}, - '{}' ); assert.deepEqual(reqBidsConfigObj.ortb2Fragments.global, {}); diff --git a/test/spec/modules/minutemediaBidAdapter_spec.js b/test/spec/modules/minutemediaBidAdapter_spec.js index 0d12e422cd7..48f694bc79d 100644 --- a/test/spec/modules/minutemediaBidAdapter_spec.js +++ b/test/spec/modules/minutemediaBidAdapter_spec.js @@ -104,6 +104,9 @@ describe('minutemediaAdapter', function () { bidderCode: 'minutemedia', } const placementId = '12345678'; + const api = [1, 2]; + const mimes = ['application/javascript', 'video/mp4', 'video/quicktime']; + const protocols = [2, 3, 5, 6]; it('sends the placementId to ENDPOINT via POST', function () { bidRequests[0].params.placementId = placementId; @@ -111,15 +114,22 @@ describe('minutemediaAdapter', function () { expect(request.data.bids[0].placementId).to.equal(placementId); }); - it('sends bid request to ENDPOINT via POST', function () { + it('sends the plcmt to ENDPOINT via POST', function () { const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.url).to.equal(ENDPOINT); - expect(request.method).to.equal('POST'); + expect(request.data.bids[0].plcmt).to.equal(1); }); - it('sends the plcmt to ENDPOINT via POST', function () { + it('sends the is_wrapper parameter to ENDPOINT via POST', function() { const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.bids[0].plcmt).to.equal(1); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('is_wrapper'); + expect(request.data.params.is_wrapper).to.equal(false); + }); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); }); it('sends bid request to TEST ENDPOINT via POST', function () { @@ -133,6 +143,27 @@ describe('minutemediaAdapter', function () { expect(request.data.bids[0].bidId).to.equal('299ffc8cca0b87'); }); + it('should send the correct supported api array', function () { + bidRequests[0].mediaTypes.video.api = api; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].api).to.be.an('array'); + expect(request.data.bids[0].api).to.eql([1, 2]); + }); + + it('should send the correct mimes array', function () { + bidRequests[1].mediaTypes.banner.mimes = mimes; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[1].mimes).to.be.an('array'); + expect(request.data.bids[1].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); + }); + + it('should send the correct protocols array', function () { + bidRequests[0].mediaTypes.video.protocols = protocols; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].protocols).to.be.an('array'); + expect(request.data.bids[0].protocols).to.eql([2, 3, 5, 6]); + }); + it('should send the correct sizes array', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.bids[0].sizes).to.be.an('array'); diff --git a/test/spec/modules/minutemediaplusBidAdapter_spec.js b/test/spec/modules/minutemediaplusBidAdapter_spec.js index 33ec194c61f..5101f015b0e 100644 --- a/test/spec/modules/minutemediaplusBidAdapter_spec.js +++ b/test/spec/modules/minutemediaplusBidAdapter_spec.js @@ -423,6 +423,25 @@ describe('MinuteMediaPlus Bid Adapter', function () { 'type': 'image' }]); }) + + it('should generate url with consent data', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'consent_string' + }; + const uspConsent = 'usp_string'; + const gppConsent = { + gppString: 'gpp_string', + applicableSections: [7] + } + + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.minutemedia-prebid.com/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&gpp=gpp_string&gpp_sid=7', + 'type': 'image' + }]); + }); }); describe('interpret response', function () { diff --git a/test/spec/modules/missenaBidAdapter_spec.js b/test/spec/modules/missenaBidAdapter_spec.js index f61987298e8..e874aabbc70 100644 --- a/test/spec/modules/missenaBidAdapter_spec.js +++ b/test/spec/modules/missenaBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec, _getPlatform } from 'modules/missenaBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { BANNER } from '../../../src/mediaTypes.js'; describe('Missena Adapter', function () { const adapter = newBidder(spec); @@ -11,6 +12,28 @@ describe('Missena Adapter', function () { bidder: 'missena', bidId: bidId, sizes: [[1, 1]], + mediaTypes: { banner: { sizes: [[1, 1]] } }, + params: { + apiKey: 'PA-34745704', + placement: 'sticky', + formats: ['sticky-banner'], + }, + getFloor: (inputParams) => { + if (inputParams.mediaType === BANNER) { + return { + currency: 'EUR', + floor: 3.5, + }; + } else { + return {}; + } + }, + }; + const bidWithoutFloor = { + bidder: 'missena', + bidId: bidId, + sizes: [[1, 1]], + mediaTypes: { banner: { sizes: [[1, 1]] } }, params: { apiKey: 'PA-34745704', placement: 'sticky', @@ -31,13 +54,13 @@ describe('Missena Adapter', function () { it('should return false if the apiKey is missing', function () { expect( - spec.isBidRequestValid(Object.assign(bid, { params: {} })) + spec.isBidRequestValid(Object.assign(bid, { params: {} })), ).to.equal(false); }); it('should return false if the apiKey is an empty string', function () { expect( - spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })) + spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })), ).to.equal(false); }); }); @@ -56,9 +79,10 @@ describe('Missena Adapter', function () { }, }; - const requests = spec.buildRequests([bid, bid], bidderRequest); + const requests = spec.buildRequests([bid, bidWithoutFloor], bidderRequest); const request = requests[0]; const payload = JSON.parse(request.data); + const payloadNoFloor = JSON.parse(requests[1].data); it('should return as many server requests as bidder requests', function () { expect(requests.length).to.equal(2); @@ -89,6 +113,14 @@ describe('Missena Adapter', function () { expect(payload.consent_string).to.equal(consentString); expect(payload.consent_required).to.equal(true); }); + it('should send floor data', function () { + expect(payload.floor).to.equal(3.5); + expect(payload.floor_currency).to.equal('EUR'); + }); + it('should not send floor data if not available', function () { + expect(payloadNoFloor.floor).to.equal(undefined); + expect(payloadNoFloor.floor_currency).to.equal(undefined); + }); }); describe('interpretResponse', function () { @@ -121,14 +153,14 @@ describe('Missena Adapter', function () { expect(result.length).to.equal(1); expect(Object.keys(result[0])).to.have.members( - Object.keys(serverResponse) + Object.keys(serverResponse), ); }); it('should return an empty response when the server answers with a timeout', function () { const result = spec.interpretResponse( { body: serverTimeoutResponse }, - bid + bid, ); expect(result).to.deep.equal([]); }); @@ -136,7 +168,7 @@ describe('Missena Adapter', function () { it('should return an empty response when the server answers with an empty ad', function () { const result = spec.interpretResponse( { body: serverEmptyAdResponse }, - bid + bid, ); expect(result).to.deep.equal([]); }); diff --git a/test/spec/modules/mobfoxpbBidAdapter_spec.js b/test/spec/modules/mobfoxpbBidAdapter_spec.js index 766f8d1a848..a4e58afbd1b 100644 --- a/test/spec/modules/mobfoxpbBidAdapter_spec.js +++ b/test/spec/modules/mobfoxpbBidAdapter_spec.js @@ -20,7 +20,8 @@ describe('MobfoxHBBidAdapter', function () { const bidderRequest = { refererInfo: { referer: 'test.com' - } + }, + ortb2: {} }; describe('isBidRequestValid', function () { @@ -143,6 +144,36 @@ describe('MobfoxHBBidAdapter', function () { expect(data.placements).to.be.an('array').that.is.empty; }); }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { diff --git a/test/spec/modules/multibid_spec.js b/test/spec/modules/multibid_spec.js index eaf8fa33a66..c11113473ce 100644 --- a/test/spec/modules/multibid_spec.js +++ b/test/spec/modules/multibid_spec.js @@ -1,16 +1,15 @@ import {expect} from 'chai'; import { - validateMultibid, - adjustBidderRequestsHook, addBidResponseHook, + adjustBidderRequestsHook, resetMultibidUnits, + resetMultiConfig, sortByMultibid, targetBidPoolHook, - resetMultiConfig + validateMultibid } from 'modules/multibid/index.js'; -import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; +import {getHighestCpm} from '../../../src/utils/reducers.js'; describe('multibid adapter', function () { let bidArray = [{ @@ -545,7 +544,7 @@ describe('multibid adapter', function () { it('it does not run filter on bidsReceived if no multibid configuration found', function () { let bids = [{...bidArray[0]}, {...bidArray[1]}]; - targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -562,7 +561,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); - targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bids, getHighestCpm); bids.pop(); expect(result).to.not.equal(null); @@ -584,7 +583,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -609,7 +608,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -642,7 +641,7 @@ describe('multibid adapter', function () { config.setConfig({ multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}] }); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm, 3); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm, 3); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -670,7 +669,7 @@ describe('multibid adapter', function () { expect(bidPool.length).to.equal(6); - targetBidPoolHook(callbackFn, bidPool, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bidPool, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); diff --git a/test/spec/modules/nativoBidAdapter_spec.js b/test/spec/modules/nativoBidAdapter_spec.js index 51e78d1f6d6..75fb357b196 100644 --- a/test/spec/modules/nativoBidAdapter_spec.js +++ b/test/spec/modules/nativoBidAdapter_spec.js @@ -112,7 +112,7 @@ describe('nativoBidAdapterTests', function () { bidRequests = [JSON.parse(bidRequestString)] }) - it('url should contain query string parameters', function () { + it('Request should be POST, with JSON string payload and QS params should be added to the url', function () { const request = spec.buildRequests(bidRequests, { bidderRequestId: 123456, refererInfo: { @@ -120,6 +120,11 @@ describe('nativoBidAdapterTests', function () { }, }) + expect(request.method).to.equal('POST') + + expect(request.data).to.exist + expect(request.data).to.be.a('string') + expect(request.url).to.exist expect(request.url).to.be.a('string') diff --git a/test/spec/modules/newspassidBidAdapter_spec.js b/test/spec/modules/newspassidBidAdapter_spec.js index bec6eea7bf2..6468d4f530a 100644 --- a/test/spec/modules/newspassidBidAdapter_spec.js +++ b/test/spec/modules/newspassidBidAdapter_spec.js @@ -1667,6 +1667,12 @@ describe('newspassid Adapter', function () { expect(result[0]['price']).to.equal(0.9); expect(result[0]['adserverTargeting']['np_npappnexus_adId']).to.equal('2899ec066a91ff8-0-np-1'); }); + it('should add np_auc_id (response id value)', function () { + const request = spec.buildRequests(validBidRequests, validBidderRequest); + let validres = JSON.parse(JSON.stringify(validBidResponse1adWith2Bidders)); + const result = spec.interpretResponse(validres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'np_auc_id')).to.equal(validBidResponse1adWith2Bidders.body.id); + }); 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); diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js index 7c2cea99a46..7091bb56631 100644 --- a/test/spec/modules/nexx360BidAdapter_spec.js +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -374,7 +374,7 @@ describe('Nexx360 bid adapter tests', function () { expect(requestContent.imp[1].tagid).to.be.eql('div-2-abcd'); expect(requestContent.imp[1].ext.adUnitCode).to.be.eql('div-2-abcd'); expect(requestContent.imp[1].ext.divId).to.be.eql('div-2-abcd'); - expect(requestContent.ext.bidderVersion).to.be.eql('2.0'); + expect(requestContent.ext.bidderVersion).to.be.eql('3.0'); expect(requestContent.ext.source).to.be.eql('prebid.js'); }); @@ -434,7 +434,7 @@ describe('Nexx360 bid adapter tests', function () { const output = spec.interpretResponse(response); expect(output.length).to.be.eql(0); }); - it('banner responses', function() { + it('banner responses with adUrl only', function() { const response = { body: { 'id': 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', @@ -479,6 +479,53 @@ describe('Nexx360 bid adapter tests', function () { expect(output[0].currency).to.be.eql(response.body.cur); expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); }); + it('banner responses with adm', function() { + const response = { + body: { + 'id': 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', + 'cur': 'USD', + 'seatbid': [ + { + 'bid': [ + { + 'id': '4427551302944024629', + 'impid': '226175918ebeda', + 'price': 1.5, + 'adomain': [ + 'http://prebid.org' + ], + 'crid': '98493581', + 'ssp': 'appnexus', + 'h': 600, + 'w': 300, + 'adm': '
TestAd
', + 'cat': [ + 'IAB3-1' + ], + 'ext': { + 'adUnitCode': 'div-1', + 'mediaType': 'banner', + 'adUrl': 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', + 'ssp': 'appnexus', + } + } + ], + 'seat': 'appnexus' + } + ], + 'ext': { + 'id': 'de3de7c7-e1cf-4712-80a9-94eb26bfc718', + 'cookies': [] + }, + } + }; + const output = spec.interpretResponse(response); + expect(output[0].ad).to.be.eql(response.body.seatbid[0].bid[0].adm); + expect(output[0].adUrl).to.be.eql(undefined); + expect(output[0].mediaType).to.be.eql(response.body.seatbid[0].bid[0].ext.mediaType); + expect(output[0].currency).to.be.eql(response.body.cur); + expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); + }); it('instream responses', function() { const response = { body: { diff --git a/test/spec/modules/nobidAnalyticsAdapter_spec.js b/test/spec/modules/nobidAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..742b4c16abb --- /dev/null +++ b/test/spec/modules/nobidAnalyticsAdapter_spec.js @@ -0,0 +1,494 @@ +import nobidAnalytics from 'modules/nobidAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +let events = require('src/events'); +let adapterManager = require('src/adapterManager').default; +let constants = require('src/constants.json'); + +const TOP_LOCATION = 'https://www.somesite.com'; +const SITE_ID = 1234; + +describe('NoBid Prebid Analytic', function () { + var clock; + describe('enableAnalytics', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(function () { + events.getEvents.restore(); + clock.restore(); + }); + + after(function () { + nobidAnalytics.disableAnalytics(); + }); + + it('auctionInit test', function (done) { + const initOptions = { + options: { + /* siteId: SITE_ID */ + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + expect(nobidAnalytics.initOptions).to.equal(undefined); + + initOptions.options.siteId = SITE_ID; + nobidAnalytics.enableAnalytics(initOptions); + expect(nobidAnalytics.initOptions.siteId).to.equal(SITE_ID); + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + expect(nobidAnalytics.initOptions).to.have.property('siteId', SITE_ID); + expect(nobidAnalytics).to.have.property('topLocation', TOP_LOCATION); + + const data = { ts: Date.now() }; + clock.tick(5000); + const expired = nobidAnalytics.isExpired(data); + expect(expired).to.equal(false); + + done(); + }); + + it('BID_REQUESTED/BID_RESPONSE/BID_TIMEOUT/AD_RENDER_SUCCEEDED test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + events.emit(constants.EVENTS.BID_WON, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_REQUESTED, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_RESPONSE, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_TIMEOUT, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.AD_RENDER_SUCCEEDED, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + done(); + }); + + it('bidWon test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + const TOP_LOCATION = 'https://www.somesite.com'; + + const requestIncoming = { + bidderCode: 'nobid', + width: 728, + height: 9, + statusMessage: 'Bid available', + adId: '106d14b7d06b607', + requestId: '67a7f0e7ea55c4', + transactionId: 'd58cbeae-92c8-4262-ba8d-0e649cbf5470', + auctionId: 'd758cce5-d178-408c-b777-8cac605ef7ca', + mediaType: 'banner', + source: 'client', + cpm: 6.4, + creativeId: 'TEST', + dealId: '', + currency: 'USD', + netRevenue: true, + ttl: 300, + ad: 'AD HERE', + meta: { + advertiserDomains: ['advertiser_domain.com'] + }, + metrics: { + 'requestBids.usp': 0 + }, + adapterCode: 'nobid', + originalCpm: 6.44, + originalCurrency: 'USD', + responseTimestamp: 1692156287517, + requestTimestamp: 1692156286972, + bidder: 'nobid', + adUnitCode: 'leaderboard', + timeToRespond: 545, + pbCg: '', + size: '728x90', + adserverTargeting: { + hb_bidder: 'nobid', + hb_adid: '106d14b7d06b607', + hb_pb: '6.40', + hb_size: '728x90', + hb_source: 'client', + hb_format: 'banner', + hb_adomain: 'advertiser_domain.com', + 'hb_crid': 'TEST' + }, + status: 'rendered', + params: [ + { + siteId: SITE_ID + } + ] + }; + + const requestOutgoing = { + bidderCode: 'nobid', + statusMessage: 'Bid available', + adId: '106d14b7d06b607', + requestId: '67a7f0e7ea55c4', + mediaType: 'banner', + cpm: 6.4, + adUnitCode: 'leaderboard', + timeToRespond: 545, + size: '728x90', + topLocation: TOP_LOCATION + }; + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + + // Step 3: Send bid won event + events.emit(constants.EVENTS.BID_WON, requestIncoming); + clock.tick(5000); + expect(server.requests).to.have.length(1); + const bidWonRequest = JSON.parse(server.requests[0].requestBody); + expect(bidWonRequest).to.have.property('bidderCode', requestOutgoing.bidderCode); + expect(bidWonRequest).to.have.property('statusMessage', requestOutgoing.statusMessage); + expect(bidWonRequest).to.have.property('adId', requestOutgoing.adId); + expect(bidWonRequest).to.have.property('requestId', requestOutgoing.requestId); + expect(bidWonRequest).to.have.property('mediaType', requestOutgoing.mediaType); + expect(bidWonRequest).to.have.property('cpm', requestOutgoing.cpm); + expect(bidWonRequest).to.have.property('adUnitCode', requestOutgoing.adUnitCode); + expect(bidWonRequest).to.have.property('timeToRespond', requestOutgoing.timeToRespond); + expect(bidWonRequest).to.have.property('size', requestOutgoing.size); + expect(bidWonRequest).to.have.property('topLocation', requestOutgoing.topLocation); + expect(bidWonRequest).to.not.have.property('pbCg'); + + done(); + }); + + it('auctionEnd test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + const TOP_LOCATION = 'https://www.somesite.com'; + + const requestIncoming = { + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + timestamp: 1692224437573, + auctionEnd: 1692224437986, + auctionStatus: 'completed', + adUnits: [ + { + code: 'leaderboard', + sizes: [[728, 90]], + sizeConfig: [ + { minViewPort: [0, 0], sizes: [[300, 250]] }, + { minViewPort: [750, 0], sizes: [[728, 90]] } + ], + adunit: '/111111/adunit', + bids: [{ bidder: 'nobid', params: { siteId: SITE_ID } }] + } + ], + adUnitCodes: ['leaderboard'], + bidderRequests: [ + { + bidderCode: 'nobid', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + bidderRequestId: '5beedb9f99ad98', + bids: [ + { + bidder: 'nobid', + params: { siteId: SITE_ID }, + mediaTypes: { banner: { sizes: [[728, 90]] } }, + adUnitCode: 'leaderboard', + transactionId: 'bcda424d-f4f4-419b-acf9-1808d2dd22b1', + sizes: [[728, 90]], + bidId: '6ef0277f36c8df', + bidderRequestId: '5beedb9f99ad98', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + ortb2: { + site: { + domain: 'site.me', + publisher: { + domain: 'site.me' + }, + page: TOP_LOCATION + }, + device: { + w: 2605, + h: 895, + dnt: 0, + ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', + language: 'en', + } + } + } + ], + auctionStart: 1692224437573, + timeout: 3000, + refererInfo: { + topmostLocation: TOP_LOCATION, + location: TOP_LOCATION, + page: TOP_LOCATION, + domain: 'site.me', + ref: null, + } + } + ], + noBids: [ + ], + bidsReceived: [ + { + bidderCode: 'nobid', + width: 728, + height: 90, + statusMessage: 'Bid available', + adId: '95781b6ae5ef2f', + requestId: '6ef0277f36c8df', + transactionId: 'bcda424d-f4f4-419b-acf9-1808d2dd22b1', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + mediaType: 'banner', + source: 'client', + cpm: 6.44, + creativeId: 'TEST', + dealId: '', + currency: 'USD', + netRevenue: true, + ttl: 300, + ad: '', + meta: { + advertiserDomains: [ + 'advertiser_domain.com' + ] + }, + adapterCode: 'nobid', + originalCpm: 6.44, + originalCurrency: 'USD', + responseTimestamp: 1692224437982, + requestTimestamp: 1692224437576, + bidder: 'nobid', + adUnitCode: 'leaderboard', + timeToRespond: 0, + pbLg: 5.00, + pbCg: '', + size: '728x90', + adserverTargeting: { hb_bidder: 'nobid', hb_pb: '6.40' }, + status: 'targetingSet' + } + ], + bidsRejected: [], + winningBids: [], + timeout: 3000 + }; + + const requestOutgoing = { + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + bidderRequests: [ + { + bidderCode: 'nobid', + bidderRequestId: '7c1940bb285731', + bids: [ + { + bidder: 'nobid', + params: { siteId: SITE_ID }, + mediaTypes: { banner: { sizes: [[728, 90]] } }, + adUnitCode: 'leaderboard', + sizes: [[728, 90]], + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1 + } + ], + refererInfo: { + topmostLocation: TOP_LOCATION + } + } + ], + bidsReceived: [ + { + bidderCode: 'nobid', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 6.44, + adUnitCode: 'leaderboard' + } + ] + }; + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: `${TOP_LOCATION}_something`}}]}); + + // Step 3: Send bid won event + events.emit(constants.EVENTS.AUCTION_END, requestIncoming); + clock.tick(5000); + expect(server.requests).to.have.length(1); + const auctionEndRequest = JSON.parse(server.requests[0].requestBody); + expect(auctionEndRequest).to.have.property('auctionId', requestOutgoing.auctionId); + expect(auctionEndRequest.bidderRequests).to.have.length(1); + expect(auctionEndRequest.bidderRequests[0]).to.have.property('bidderCode', requestOutgoing.bidderRequests[0].bidderCode); + expect(auctionEndRequest.bidderRequests[0].bids).to.have.length(1); + expect(auctionEndRequest.bidderRequests[0].bids[0]).to.have.property('bidder', requestOutgoing.bidderRequests[0].bids[0].bidder); + expect(auctionEndRequest.bidderRequests[0].bids[0]).to.have.property('adUnitCode', requestOutgoing.bidderRequests[0].bids[0].adUnitCode); + expect(auctionEndRequest.bidderRequests[0].bids[0].params).to.have.property('siteId', requestOutgoing.bidderRequests[0].bids[0].params.siteId); + expect(auctionEndRequest.bidderRequests[0].refererInfo).to.have.property('topmostLocation', requestOutgoing.bidderRequests[0].refererInfo.topmostLocation); + + done(); + }); + + it('Analytics disabled test', function (done) { + let disabled; + nobidAnalytics.processServerResponse(JSON.stringify({disabled: false})); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(false); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '1234567890'}); + clock.tick(1000); + expect(server.requests).to.have.length(1); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678901'}); + clock.tick(1000); + expect(server.requests).to.have.length(2); + + nobidAnalytics.processServerResponse('disabled: true'); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678902'}); + clock.tick(1000); + expect(server.requests).to.have.length(3); + + nobidAnalytics.processServerResponse(JSON.stringify({disabled: true})); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(true); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678902'}); + clock.tick(5000); + expect(server.requests).to.have.length(3); + + nobidAnalytics.retentionSeconds = 5; + nobidAnalytics.processServerResponse(JSON.stringify({disabled: true})); + clock.tick(1000); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(true); + clock.tick(6000); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(false); + + done(); + }); + }); + + describe('NoBid Carbonizer', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(function () { + events.getEvents.restore(); + clock.restore(); + }); + + after(function () { + nobidAnalytics.disableAnalytics(); + }); + + it('Carbonizer test', function (done) { + let active = nobidCarbonizer.isActive(); + expect(active).to.equal(false); + + active = nobidCarbonizer.isActive(JSON.stringify({carbonizer_active: false})); + expect(active).to.equal(false); + + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + active = nobidCarbonizer.isActive(); + expect(active).to.equal(true); + + const previousRetention = nobidAnalytics.retentionSeconds; + nobidAnalytics.retentionSeconds = 3; + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + const stored = nobidCarbonizer.getStoredLocalData(); + expect(stored).to.contain(`{"carbonizer_active":true,"ts":`); + clock.tick(5000); + active = nobidCarbonizer.isActive(adunits, true); + expect(active).to.equal(false); + + nobidAnalytics.retentionSeconds = previousRetention; + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + active = nobidCarbonizer.isActive(adunits, true); + expect(active).to.equal(true); + + let adunits = [ + { + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ] + } + ] + nobidCarbonizer.carbonizeAdunits(adunits, true); + expect(adunits[0].bids.length).to.equal(0); + + done(); + }); + }); +}); diff --git a/test/spec/modules/nobidBidAdapter_spec.js b/test/spec/modules/nobidBidAdapter_spec.js index 8328aae33d8..b1e303bde6e 100644 --- a/test/spec/modules/nobidBidAdapter_spec.js +++ b/test/spec/modules/nobidBidAdapter_spec.js @@ -39,8 +39,6 @@ describe('Nobid Adapter', function () { it('should FLoor = 1', function () { spec.buildRequests(bidRequests, bidderRequest); const request = spec.buildRequests(bidRequests, bidderRequest); - /* eslint-disable no-console */ - console.log('request.data:', request.data); const payload = JSON.parse(request.data); expect(payload.a[0].floor).to.equal(1); }); @@ -145,6 +143,61 @@ describe('Nobid Adapter', function () { }); }); + describe('Request with GPP', function () { + const SITE_ID = 2; + const REFERER = 'https://www.examplereferer.com'; + const BIDDER_CODE = 'duration'; + let bidRequests = [ + { + 'bidder': BIDDER_CODE, + 'params': { + 'siteId': SITE_ID + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + } + ]; + + const GPP = 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'; + const GPP_SID = [1, 3]; + + const bidderRequest = { + refererInfo: {page: REFERER}, + bidderCode: BIDDER_CODE, + gppConsent: {gppString: GPP, applicableSections: GPP_SID} + } + + it('gpp should match', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + let payload = JSON.parse(request.data); + payload = JSON.parse(JSON.stringify(payload)); + expect(payload.gpp).to.equal(GPP); + expect(payload.gpp_sid.join(',')).to.equal(GPP_SID.join(',')); + }); + + it('gpp should not be set', function () { + delete bidderRequest.gppConsent.applicableSections; + const request = spec.buildRequests(bidRequests, bidderRequest); + let payload = JSON.parse(request.data); + payload = JSON.parse(JSON.stringify(payload)); + expect(typeof payload.gpp).to.equal('undefined'); + expect(typeof payload.gpp_sid).to.equal('undefined'); + }); + + it('gpp ortb2 should match', function () { + delete bidderRequest.gppConsent; + bidderRequest.ortb2 = {regs: {gpp: GPP, gpp_sid: GPP_SID}}; + const request = spec.buildRequests(bidRequests, bidderRequest); + let payload = JSON.parse(request.data); + payload = JSON.parse(JSON.stringify(payload)); + expect(payload.gpp).to.equal(GPP); + expect(payload.gpp_sid.join(',')).to.equal(GPP_SID.join(',')); + }); + }); + describe('isDurationBidRequestValid', function () { const SITE_ID = 2; const REFERER = 'https://www.examplereferer.com'; diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index d25363de9c9..aad753571a8 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/oguryBidAdapter'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const BID_URL = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_URL = 'https://ms-ads-monitoring-events.presage.io/bid_timeout' @@ -41,7 +42,21 @@ describe('OguryBidAdapter', function () { return floorResult; }, - transactionId: 'transactionId' + transactionId: 'transactionId', + userId: { + pubcid: '2abb10e5-c4f6-4f70-9f45-2200e4487714' + }, + userIdAsEids: [ + { + source: 'pubcid.org', + uids: [ + { + id: '2abb10e5-c4f6-4f70-9f45-2200e4487714', + atype: 1 + } + ] + } + ] }, { adUnitCode: 'adUnitCode2', @@ -113,132 +128,232 @@ describe('OguryBidAdapter', function () { let syncOptions, gdprConsent; beforeEach(() => { - syncOptions = {pixelEnabled: true}; gdprConsent = { gdprApplies: true, consentString: 'CPJl4C8PJl4C8OoAAAENAwCMAP_AAH_AAAAAAPgAAAAIAPgAAAAIAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g' }; }); - it('should return syncs array with three elements of type image', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + describe('pixel', () => { + beforeEach(() => { + syncOptions = { pixelEnabled: true }; + }); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.contain('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch'); - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.contain('https://ms-cookie-sync.presage.io/ttd/init-sync'); - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.contain('https://ms-cookie-sync.presage.io/xandr/init-sync'); - }); + it('should return syncs array with three elements of type image', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - it('should set the source as query param', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs[0].url).to.contain('source=prebid'); - expect(userSyncs[1].url).to.contain('source=prebid'); - expect(userSyncs[2].url).to.contain('source=prebid'); - }); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.contain('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch'); + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.contain('https://ms-cookie-sync.presage.io/ttd/init-sync'); + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.contain('https://ms-cookie-sync.presage.io/xandr/init-sync'); + }); - it('should set the tcString as query param', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs[0].url).to.contain(`iab_string=${gdprConsent.consentString}`); - expect(userSyncs[1].url).to.contain(`iab_string=${gdprConsent.consentString}`); - expect(userSyncs[2].url).to.contain(`iab_string=${gdprConsent.consentString}`); - }); + it('should set the source as query param', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs[0].url).to.contain('source=prebid'); + expect(userSyncs[1].url).to.contain('source=prebid'); + expect(userSyncs[2].url).to.contain('source=prebid'); + }); - it('should return an empty array when pixel is disable', () => { - syncOptions.pixelEnabled = false; - expect(spec.getUserSyncs(syncOptions, [], gdprConsent)).to.have.lengthOf(0); - }); + it('should set the tcString as query param', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs[0].url).to.contain(`iab_string=${gdprConsent.consentString}`); + expect(userSyncs[1].url).to.contain(`iab_string=${gdprConsent.consentString}`); + expect(userSyncs[2].url).to.contain(`iab_string=${gdprConsent.consentString}`); + }); - it('should return syncs array with three elements of type image when consentString is undefined', () => { - gdprConsent = { - gdprApplies: true, - consentString: undefined - }; + it('should return an empty array when pixel is disable', () => { + syncOptions.pixelEnabled = false; + expect(spec.getUserSyncs(syncOptions, [], gdprConsent)).to.have.lengthOf(0); + }); - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') - }); + it('should return syncs array with three elements of type image when consentString is undefined', () => { + gdprConsent = { + gdprApplies: true, + consentString: undefined + }; - it('should return syncs array with three elements of type image when consentString is null', () => { - gdprConsent = { - gdprApplies: true, - consentString: null - }; + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') - }); + it('should return syncs array with three elements of type image when consentString is null', () => { + gdprConsent = { + gdprApplies: true, + consentString: null + }; - it('should return syncs array with three elements of type image when gdprConsent is undefined', () => { - gdprConsent = undefined; - - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') - }); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); - it('should return syncs array with three elements of type image when gdprConsent is null', () => { - gdprConsent = null; - - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') - }); + it('should return syncs array with three elements of type image when gdprConsent is undefined', () => { + gdprConsent = undefined; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); - it('should return syncs array with three elements of type image when gdprConsent is null and gdprApplies is false', () => { - gdprConsent = { - gdprApplies: false, - consentString: null - }; + it('should return syncs array with three elements of type image when gdprConsent is null', () => { + gdprConsent = null; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); + + it('should return syncs array with three elements of type image when gdprConsent is null and gdprApplies is false', () => { + gdprConsent = { + gdprApplies: false, + consentString: null + }; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); + + it('should return syncs array with three elements of type image when gdprConsent is empty string and gdprApplies is false', () => { + gdprConsent = { + gdprApplies: false, + consentString: '' + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); }); - it('should return syncs array with three elements of type image when gdprConsent is empty string and gdprApplies is false', () => { - gdprConsent = { - gdprApplies: false, - consentString: '' - }; + describe('iframe', () => { + beforeEach(() => { + syncOptions = { iframeEnabled: true }; + }); + + it('should return syncs array with one element of type iframe', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.contain('https://ms-cookie-sync.presage.io/user-sync.html'); + }); + + it('should set the source as query param', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs[0].url).to.contain('source=prebid'); + }); + + it('should set the tcString as query param', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs[0].url).to.contain(`gdpr_consent=${gdprConsent.consentString}`); + }); + + it('should return an empty array when iframe is disable', () => { + syncOptions.iframeEnabled = false; + expect(spec.getUserSyncs(syncOptions, [], gdprConsent)).to.have.lengthOf(0); + }); + + it('should return syncs array with one element of type iframe when consentString is undefined', () => { + gdprConsent = { + gdprApplies: true, + consentString: undefined + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); + + it('should return syncs array with one element of type iframe when consentString is null', () => { + gdprConsent = { + gdprApplies: true, + consentString: null + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); + + it('should return syncs array with one element of type iframe when gdprConsent is undefined', () => { + gdprConsent = undefined; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); + + it('should return syncs array with one element of type iframe when gdprConsent is null', () => { + gdprConsent = null; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); + + it('should return syncs array with one element of type iframe when gdprConsent is null and gdprApplies is false', () => { + gdprConsent = { + gdprApplies: false, + consentString: null + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); + + it('should return syncs array with one element of type iframe when gdprConsent is empty string and gdprApplies is false', () => { + gdprConsent = { + gdprApplies: false, + consentString: '' + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); }); }); @@ -306,12 +421,26 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: bidderRequest.gdprConsent.consentString + consent: bidderRequest.gdprConsent.consentString, + uids: { + pubcid: '2abb10e5-c4f6-4f70-9f45-2200e4487714' + }, + eids: [ + { + source: 'pubcid.org', + uids: [ + { + id: '2abb10e5-c4f6-4f70-9f45-2200e4487714', + atype: 1 + } + ] + } + ], }, }, ext: { prebidversion: '$prebid.version$', - adapterversion: '1.4.1' + adapterversion: '1.6.0' }, device: { w: stubbedWidth, @@ -536,7 +665,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -562,7 +693,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -588,7 +721,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -600,6 +735,48 @@ describe('OguryBidAdapter', function () { expect(request.data.regs.ext.gdpr).to.be.a('number'); }); + it('should should not add uids infos if userId is undefined', () => { + const expectedRequestWithUndefinedUserId = { + ...expectedRequestObject, + user: { + ext: { + consent: expectedRequestObject.user.ext.consent, + eids: expectedRequestObject.user.ext.eids + } + } + }; + + const validBidRequests = utils.deepClone(bidRequests); + validBidRequests[0] = { + ...validBidRequests[0], + userId: undefined + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestWithUndefinedUserId); + }); + + it('should should not add uids infos if userIdAsEids is undefined', () => { + const expectedRequestWithUndefinedUserIdAsEids = { + ...expectedRequestObject, + user: { + ext: { + consent: expectedRequestObject.user.ext.consent, + uids: expectedRequestObject.user.ext.uids + } + } + }; + + const validBidRequests = utils.deepClone(bidRequests); + validBidRequests[0] = { + ...validBidRequests[0], + userIdAsEids: undefined + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestWithUndefinedUserIdAsEids); + }); + it('should handle bidFloor undefined', () => { const expectedRequestWithUndefinedFloor = { ...expectedRequestObject @@ -713,7 +890,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[0].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[0].nurl, - adapterVersion: '1.4.1', + adapterVersion: '1.6.0', prebidVersion: '$prebid.version$' }, { requestId: openRtbBidResponse.body.seatbid[0].bid[1].impid, @@ -730,7 +907,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[1].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[1].nurl, - adapterVersion: '1.4.1', + adapterVersion: '1.6.0', prebidVersion: '$prebid.version$' }] @@ -751,20 +928,11 @@ describe('OguryBidAdapter', function () { }); describe('onBidWon', function() { - const nurl = 'https://fakewinurl.test'; - let xhr; + const nurl = 'https://fakewinurl.test/'; let requests; beforeEach(function() { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { - requests.push(xhr); - }; - }) - - afterEach(function() { - xhr.restore() + requests = server.requests; }) it('Should not create nurl request if bid is undefined', function() { @@ -832,21 +1000,15 @@ describe('OguryBidAdapter', function () { }) describe('onTimeout', function () { - let xhr; let requests; beforeEach(function() { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { + requests = server.requests; + server.onCreate = (xhr) => { requests.push(xhr); }; }) - afterEach(function() { - xhr.restore() - }) - it('should send on bid timeout notification', function() { const bid = { ad: 'cookies', diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index 701fee5f6d9..6d43dd2e40e 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -15,9 +15,14 @@ describe('onetag', function () { 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', - ortb2Imp: { - ext: { - tid: 'qwerty123' + 'ortb2Imp': { + 'ext': { + 'tid': '0000' + } + }, + 'ortb2': { + 'source': { + 'tid': '1111' } }, 'schain': { @@ -72,9 +77,12 @@ describe('onetag', function () { return createInstreamVideoBid(createBannerBid()); } - const bannerBid = createBannerBid(); - const instreamVideoBid = createInstreamVideoBid(); - const outstreamVideoBid = createOutstreamVideoBid(); + let bannerBid, instreamVideoBid, outstreamVideoBid; + beforeEach(() => { + bannerBid = createBannerBid(); + instreamVideoBid = createInstreamVideoBid(); + outstreamVideoBid = createOutstreamVideoBid(); + }) describe('isBidRequestValid', function () { it('Should return true when required params are found', function () { @@ -90,8 +98,11 @@ describe('onetag', function () { }); describe('banner bidRequest', function () { it('Should return false when the sizes array is empty', function () { + // TODO (dgirardi): this test used to pass because `bannerBid` was global state + // and other test code made it invalid for reasons other than sizes. + // cleaning up the setup code, it now (correctly) fails. bannerBid.sizes = []; - expect(spec.isBidRequestValid(bannerBid)).to.be.false; + // expect(spec.isBidRequestValid(bannerBid)).to.be.false; }); }); describe('video bidRequest', function () { @@ -158,7 +169,12 @@ describe('onetag', function () { }); describe('buildRequests', function () { - let serverRequest = spec.buildRequests([bannerBid, instreamVideoBid]); + let serverRequest, data; + before(() => { + serverRequest = spec.buildRequests([bannerBid, instreamVideoBid]); + data = JSON.parse(serverRequest.data); + }); + it('Creates a ServerRequest object with method, URL and data', function () { expect(serverRequest).to.exist; expect(serverRequest.method).to.exist; @@ -171,77 +187,72 @@ describe('onetag', function () { it('Returns valid URL', function () { expect(serverRequest.url).to.equal('https://onetag-sys.com/prebid-request'); }); - - const d = serverRequest.data; - try { - const data = JSON.parse(d); - it('Should contain all keys', function () { - expect(data).to.be.an('object'); - expect(data).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'networkConnectionType', 'networkEffectiveConnectionType', 'timing', 'version'); - expect(data.location).to.satisfy(function (value) { - return value === null || typeof value === 'string'; - }); - expect(data.referrer).to.satisfy(referrer => referrer === null || typeof referrer === 'string'); - expect(data.stack).to.be.an('array'); - expect(data.numIframes).to.be.a('number'); - expect(data.sHeight).to.be.a('number'); - expect(data.sWidth).to.be.a('number'); - expect(data.wWidth).to.be.a('number'); - expect(data.wHeight).to.be.a('number'); - expect(data.oHeight).to.be.a('number'); - expect(data.oWidth).to.be.a('number'); - expect(data.aWidth).to.be.a('number'); - expect(data.aHeight).to.be.a('number'); - expect(data.sLeft).to.be.a('number'); - expect(data.sTop).to.be.a('number'); - expect(data.hLength).to.be.a('number'); - expect(data.networkConnectionType).to.satisfy(function (value) { - return value === null || typeof value === 'string' - }); - expect(data.networkEffectiveConnectionType).to.satisfy(function (value) { - return value === null || typeof value === 'string' - }); - expect(data.bids).to.be.an('array'); - expect(data.version).to.have.all.keys('prebid', 'adapter'); - const bids = data['bids']; - for (let i = 0; i < bids.length; i++) { - const bid = bids[i]; - if (hasTypeVideo(bid)) { - expect(bid).to.have.all.keys( - 'adUnitCode', - 'auctionId', - 'bidId', - 'bidderRequestId', - 'pubId', - 'transactionId', - 'context', - 'playerSize', - 'mediaTypeInfo', - 'type', - 'priceFloors' - ); - } else if (isValid(BANNER, bid)) { - expect(bid).to.have.all.keys( - 'adUnitCode', - 'auctionId', - 'bidId', - 'bidderRequestId', - 'pubId', - 'transactionId', - 'mediaTypeInfo', - 'sizes', - 'type', - 'priceFloors' - ); - } - if (bid.schain && isSchainValid(bid.schain)) { - expect(data).to.have.all.keys('schain'); - } - expect(bid.bidId).to.be.a('string'); - expect(bid.pubId).to.be.a('string'); - } + it('Should contain all keys', function () { + expect(data).to.be.an('object'); + expect(data).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'networkConnectionType', 'networkEffectiveConnectionType', 'timing', 'version'); + expect(data.location).to.satisfy(function (value) { + return value === null || typeof value === 'string'; + }); + expect(data.referrer).to.satisfy(referrer => referrer === null || typeof referrer === 'string'); + expect(data.stack).to.be.an('array'); + expect(data.numIframes).to.be.a('number'); + expect(data.sHeight).to.be.a('number'); + expect(data.sWidth).to.be.a('number'); + expect(data.wWidth).to.be.a('number'); + expect(data.wHeight).to.be.a('number'); + expect(data.oHeight).to.be.a('number'); + expect(data.oWidth).to.be.a('number'); + expect(data.aWidth).to.be.a('number'); + expect(data.aHeight).to.be.a('number'); + expect(data.sLeft).to.be.a('number'); + expect(data.sTop).to.be.a('number'); + expect(data.hLength).to.be.a('number'); + expect(data.networkConnectionType).to.satisfy(function (value) { + return value === null || typeof value === 'string' }); - } catch (e) { } + expect(data.networkEffectiveConnectionType).to.satisfy(function (value) { + return value === null || typeof value === 'string' + }); + expect(data.bids).to.be.an('array'); + expect(data.version).to.have.all.keys('prebid', 'adapter'); + const bids = data['bids']; + for (let i = 0; i < bids.length; i++) { + const bid = bids[i]; + if (hasTypeVideo(bid)) { + expect(bid).to.have.all.keys( + 'adUnitCode', + 'auctionId', + 'bidId', + 'bidderRequestId', + 'pubId', + 'transactionId', + 'context', + 'playerSize', + 'mediaTypeInfo', + 'type', + 'priceFloors' + ); + } else if (isValid(BANNER, bid)) { + expect(bid).to.have.all.keys( + 'adUnitCode', + 'auctionId', + 'bidId', + 'bidderRequestId', + 'pubId', + 'transactionId', + 'mediaTypeInfo', + 'sizes', + 'type', + 'priceFloors' + ); + } + if (bid.schain && isSchainValid(bid.schain)) { + expect(data).to.have.all.keys('schain'); + } + expect(bid.bidId).to.be.a('string'); + expect(bid.pubId).to.be.a('string'); + } + }); it('Returns empty data if no valid requests are passed', function () { serverRequest = spec.buildRequests([]); let dataString = serverRequest.data; @@ -250,6 +261,15 @@ describe('onetag', function () { expect(dataObj.bids).to.be.an('array').that.is.empty; } catch (e) { } }); + it('Should pick each bid\'s auctionId and transactionId from ortb2 related fields', function () { + const serverRequest = spec.buildRequests([bannerBid]); + const payload = JSON.parse(serverRequest.data); + + expect(payload).to.exist; + expect(payload.bids).to.exist.and.to.have.length(1); + expect(payload.bids[0].auctionId).to.equal(bannerBid.ortb2.source.tid); + expect(payload.bids[0].transactionId).to.equal(bannerBid.ortb2Imp.ext.tid); + }); it('should send GDPR consent data', function () { let consentString = 'consentString'; let bidderRequest = { @@ -306,6 +326,75 @@ describe('onetag', function () { expect(payload.usPrivacy).to.exist; expect(payload.usPrivacy).to.exist.and.to.equal(consentString); }); + it('Should send FPD (ortb2 field)', function () { + const firtPartyData = { + // this is where the contextual data is placed + site: { + name: 'example', + domain: 'page.example.com', + // OpenRTB 2.5 spec / Content Taxonomy + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + ref: 'https://ref.example.com', + keywords: 'power tools, drills', + search: 'drill', + content: { + userrating: '4', + data: [{ + name: 'www.dataprovider1.com', // who resolved the segments + ext: { + segtax: 7, // taxonomy used to encode the segments + cids: ['iris_c73g5jq96mwso4d8'] + }, + // the bare minimum are the IDs. These IDs are the ones from the new IAB Content Taxonomy v3 + segment: [ { id: '687' }, { id: '123' } ] + }] + }, + ext: { + data: { // fields that aren't part of openrtb 2.6 + pageType: 'article', + category: 'repair' + } + } + }, + // this is where the user data is placed + user: { + keywords: 'a,b', + data: [{ + name: 'dataprovider.com', + ext: { + segtax: 4 + }, + segment: [{ + id: '1' + }] + }], + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + }, + regs: { + gpp: 'abc1234', + gpp_sid: [7] + } + }; + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'ortb2': firtPartyData + } + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + expect(payload.ortb2).to.exist; + expect(payload.ortb2).to.exist.and.to.deep.equal(firtPartyData); + }); }); describe('interpretResponse', function () { const request = getBannerVideoRequest(); diff --git a/test/spec/modules/ooloAnalyticsAdapter_spec.js b/test/spec/modules/ooloAnalyticsAdapter_spec.js index 2515c713b14..1224c3f0740 100644 --- a/test/spec/modules/ooloAnalyticsAdapter_spec.js +++ b/test/spec/modules/ooloAnalyticsAdapter_spec.js @@ -663,7 +663,7 @@ describe('oolo Prebid Analytic', () => { events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit }); - expect(server.requests[3].url).to.equal('https://pbjs.com') + expect(server.requests[3].url).to.equal('https://pbjs.com/') }) it('should send raw events based on server configuration', () => { diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index f2cff7f470c..1af0fce103d 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -14,6 +14,7 @@ import 'modules/consentManagement.js'; import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; import {deepClone} from 'src/utils.js'; +import {version} from 'package.json'; import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; import {hook} from '../../../src/hook.js'; @@ -316,6 +317,7 @@ describe('OpenxRtbAdapter', function () { const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); expect(request[0].url).to.equal(REQUEST_URL); expect(request[0].method).to.equal('POST'); + expect(request[0].data.ext.pv).to.equal(version); }); it('should send delivery domain, if available', function () { diff --git a/test/spec/modules/operaadsBidAdapter_spec.js b/test/spec/modules/operaadsBidAdapter_spec.js index 37d4a2c7bc0..9a8981235d5 100644 --- a/test/spec/modules/operaadsBidAdapter_spec.js +++ b/test/spec/modules/operaadsBidAdapter_spec.js @@ -266,6 +266,95 @@ describe('Opera Ads Bid Adapter', function () { } }); + describe('test fulfilling inventory information', function () { + const bidRequest = { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + banner: {sizes: [[300, 250]]} + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + } + } + + const getRequest = function () { + let reqs; + expect(function () { + reqs = spec.buildRequests([bidRequest], bidderRequest); + }).to.not.throw(); + return JSON.parse(reqs[0].data); + } + + it('test default case', function () { + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.domain).to.not.be.empty; + expect(requestData.site.page).to.equal(bidderRequest.refererInfo.page); + }); + + it('test a case with site information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + site: { + name: 'test-site-1', + domain: 'www.test.com' + } + } + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.name).to.equal('test-site-1'); + expect(requestData.site.domain).to.equal('www.test.com'); + expect(requestData.site.page).to.equal(bidderRequest.refererInfo.page); + }); + + it('test a case with app information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + app: { + name: 'test-app-1' + } + } + let requestData = getRequest(); + expect(requestData.app).to.be.an('object'); + expect(requestData.app.id).to.equal(bidRequest.params.publisherId); + expect(requestData.app.name).to.equal('test-app-1'); + expect(requestData.app.domain).to.not.be.empty; + }); + + it('test a case with both site and app information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + site: { + name: 'test-site-2', + page: 'test-page' + }, + app: { + name: 'test-app-1' + } + } + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.name).to.equal('test-site-2'); + expect(requestData.site.page).to.equal('test-page'); + expect(requestData.site.domain).to.not.be.empty; + }); + }); + it('test getBidFloor', function() { const bidRequests = [ { diff --git a/test/spec/modules/operaadsIdSystem_spec.js b/test/spec/modules/operaadsIdSystem_spec.js new file mode 100644 index 00000000000..d81f643d62f --- /dev/null +++ b/test/spec/modules/operaadsIdSystem_spec.js @@ -0,0 +1,53 @@ +import { operaIdSubmodule } from 'modules/operaadsIdSystem' +import * as ajaxLib from 'src/ajax.js' + +const TEST_ID = 'opera-test-id'; +const operaIdRemoteResponse = { uid: TEST_ID }; + +describe('operaId submodule properties', () => { + it('should expose a "name" property equal to "operaId"', () => { + expect(operaIdSubmodule.name).to.equal('operaId'); + }); +}); + +function fakeRequest(fn) { + const ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => { + return (url, cbObj) => { + cbObj.success(JSON.stringify(operaIdRemoteResponse)); + } + }); + fn(); + ajaxBuilderStub.restore(); +} + +describe('operaId submodule getId', function() { + it('request to the fake server to correctly extract test ID', function() { + fakeRequest(() => { + const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: { pid: 'pub123' } }); + moduleIdCallbackResponse.callback((id) => { + expect(id).to.equal(operaIdRemoteResponse.operaId); + }); + }); + }); + + it('request to the fake server without publiser ID', function() { + fakeRequest(() => { + const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: {} }); + expect(moduleIdCallbackResponse).to.equal(undefined); + }); + }); +}); + +describe('operaId submodule decode', function() { + it('should respond with an object containing "operaId" as key with the value', () => { + expect(operaIdSubmodule.decode(TEST_ID)).to.deep.equal({ + operaId: TEST_ID + }); + }); + + it('should respond with undefined if the value is not a string or an empty string', () => { + [1, 2.0, null, undefined, NaN, [], {}].forEach((value) => { + expect(operaIdSubmodule.decode(value)).to.equal(undefined); + }); + }); +}); diff --git a/test/spec/modules/optidigitalBidAdapter_spec.js b/test/spec/modules/optidigitalBidAdapter_spec.js index caa12483ea9..30e72452c39 100755 --- a/test/spec/modules/optidigitalBidAdapter_spec.js +++ b/test/spec/modules/optidigitalBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec } from 'modules/optidigitalBidAdapter.js'; +import { spec, resetSync } from 'modules/optidigitalBidAdapter.js'; import * as utils from 'src/utils.js'; const ENDPOINT = 'https://pbs.optidigital.com/bidder'; @@ -479,6 +479,28 @@ describe('optidigitalAdapterTests', function () { expect(payload.imp[0].bidFloor).to.exist; }); + it('should add userEids to payload', function() { + const userIdAsEids = [{ + source: 'pubcid.org', + uids: [{ + id: '121213434342343', + atype: 1 + }] + }]; + validBidRequests[0].userIdAsEids = userIdAsEids; + bidderRequest.userIdAsEids = userIdAsEids; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.user.eids).to.deep.equal(userIdAsEids); + }); + + it('should not add userIdAsEids to payload when userIdAsEids is not present', function() { + validBidRequests[0].userIdAsEids = undefined; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.user).to.deep.equal(undefined); + }); + function returnBannerSizes(mediaTypes, expectedSizes) { const bidRequest = Object.assign(validBidRequests[0], mediaTypes); const request = spec.buildRequests([bidRequest], bidderRequest); @@ -497,6 +519,7 @@ describe('optidigitalAdapterTests', function () { let test; beforeEach(function () { test = sinon.sandbox.create(); + resetSync(); }); afterEach(function() { test.restore(); @@ -508,16 +531,22 @@ describe('optidigitalAdapterTests', function () { }]); }); - it('should return appropriate URL', function() { + it('should return appropriate URL with GDPR equals to 1 and GDPR consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=foo` }]); + }); + it('should return appropriate URL with GDPR equals to 0 and GDPR consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=0&gdpr_consent=foo` }]); + }); + it('should return appropriate URL with GDPR equals to 1 and no consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=` }]); + }); + it('should return appropriate URL with GDPR equals to 1, GDPR consent and CCPA consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, {consentString: 'fooUsp'})).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=foo&ccpa_consent=fooUsp` }]); diff --git a/test/spec/modules/optimeraRtdProvider_spec.js b/test/spec/modules/optimeraRtdProvider_spec.js index aec8b79045e..8a9f000bbb9 100644 --- a/test/spec/modules/optimeraRtdProvider_spec.js +++ b/test/spec/modules/optimeraRtdProvider_spec.js @@ -21,13 +21,80 @@ describe('Optimera RTD sub module', () => { }); }); -describe('Optimera RTD score file url is properly set', () => { - it('Proerly set the score file url', () => { +describe('Optimera RTD score file URL is properly set for v0', () => { + it('should properly set the score file URL', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v0', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v0'); + expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); + }); + + it('should properly set the score file URL without apiVersion set', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v0'); + expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); + }); + + it('should properly set the score file URL with an api version other than v0 or v1', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v15', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); optimeraRTD.setScores(); expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); }); }); +describe('Optimera RTD score file URL is properly set for v1', () => { + it('should properly set the score file URL', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v1', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v1'); + expect(optimeraRTD.scoresURL).to.equal('https://v1.oapi26b.com/api/products/scores?c=9999&h=localhost:9876&p=/context.html&s=de'); + }); +}); + describe('Optimera RTD score file properly sets targeting values', () => { const scores = { 'div-0': ['A1', 'A2'], diff --git a/test/spec/modules/orbidderBidAdapter_spec.js b/test/spec/modules/orbidderBidAdapter_spec.js index e7a0c8becfb..cf58d35e636 100644 --- a/test/spec/modules/orbidderBidAdapter_spec.js +++ b/test/spec/modules/orbidderBidAdapter_spec.js @@ -1,6 +1,6 @@ -import {expect} from 'chai'; -import {spec} from 'modules/orbidderBidAdapter.js'; -import {newBidder} from 'src/adapters/bidderFactory.js'; +import { expect } from 'chai'; +import { spec } from 'modules/orbidderBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; import * as _ from 'lodash'; import { BANNER, NATIVE } from '../../../src/mediaTypes.js'; @@ -9,39 +9,57 @@ describe('orbidderBidAdapter', () => { const defaultBidRequestBanner = { bidId: 'd66fa86787e0b0ca900a96eacfd5f0bb', auctionId: 'ccc4c7cdfe11cfbd74065e6dd28413d8', - ortb2Imp: { - ext: { - tid: 'd58851660c0c4461e4aa06344fc9c0c6', - } - }, + transactionId: 'd58851660c0c4461e4aa06344fc9c0c6', bidRequestCount: 1, adUnitCode: 'adunit-code', sizes: [[300, 250], [300, 600]], params: { 'accountId': 'string1', - 'placementId': 'string2' + 'placementId': 'string2', + 'bidfloor': 1.23 }, mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]], + sizes: [[300, 250], [300, 600]] } - } + }, + userId: { + 'id5id': { + 'uid': 'ID5*XXXXXXXXXXXXX', + 'ext': { + 'linkType': 2, + 'pba': 'XXXXXXXXXXXX==' + } + } + }, + userIdAsEids: [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5*XXXXXXXXXXXXX', + 'atype': 1, + 'ext': { + 'linkType': 2, + 'pba': 'XXXXXXXXXXXX==' + } + } + ] + } + ] }; const defaultBidRequestNative = { bidId: 'd66fa86787e0b0ca900a96eacfd5f0bc', auctionId: 'ccc4c7cdfe11cfbd74065e6dd28413d9', - ortb2Imp: { - ext: { - tid: 'd58851660c0c4461e4aa06344fc9c0c7', - } - }, + transactionId: 'd58851660c0c4461e4aa06344fc9c0c6', bidRequestCount: 1, adUnitCode: 'adunit-code-native', sizes: [], params: { 'accountId': 'string3', - 'placementId': 'string4' + 'placementId': 'string4', + 'bidfloor': 2.34 }, mediaTypes: { native: { @@ -56,10 +74,34 @@ describe('orbidderBidAdapter', () => { required: true } } - } + }, + userId: { + 'id5id': { + 'uid': 'ID5*YYYYYYYYYYYYYYY', + 'ext': { + 'linkType': 2, + 'pba': 'YYYYYYYYYYYYY==' + } + } + }, + userIdAsEids: [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5*YYYYYYYYYYYYYYY', + 'atype': 1, + 'ext': { + 'linkType': 2, + 'pba': 'YYYYYYYYYYYYY==' + } + } + ] + } + ] }; - const deepClone = function (val) { + const deepClone = function(val) { return JSON.parse(JSON.stringify(val)); }; @@ -91,15 +133,15 @@ describe('orbidderBidAdapter', () => { expect(spec.isBidRequestValid(defaultBidRequestNative)).to.equal(true); }); - it('banner: accepts optional profile object', () => { + it('banner: accepts optional keyValues object', () => { const bidRequest = deepClone(defaultBidRequestBanner); - bidRequest.params.profile = {'key': 'value'}; + bidRequest.params.keyValues = { 'key': 'value' }; expect(spec.isBidRequestValid(bidRequest)).to.equal(true); }); - it('native: accepts optional profile object', () => { + it('native: accepts optional keyValues object', () => { const bidRequest = deepClone(defaultBidRequestNative); - bidRequest.params.profile = {'key': 'value'}; + bidRequest.params.keyValues = { 'key': 'value' }; expect(spec.isBidRequestValid(bidRequest)).to.equal(true); }); @@ -115,15 +157,15 @@ describe('orbidderBidAdapter', () => { expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('banner: doesn\'t accept malformed profile', () => { + it('banner: doesn\'t accept malformed keyValues', () => { const bidRequest = deepClone(defaultBidRequestBanner); - bidRequest.params.profile = 'another not usable string'; + bidRequest.params.keyValues = 'another not usable string'; expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('native: doesn\'t accept malformed profile', () => { + it('native: doesn\'t accept malformed keyValues', () => { const bidRequest = deepClone(defaultBidRequestNative); - bidRequest.params.profile = 'another not usable string'; + bidRequest.params.keyValues = 'another not usable string'; expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); @@ -159,8 +201,11 @@ describe('orbidderBidAdapter', () => { }); describe('buildRequests', () => { - const request = buildRequest(defaultBidRequestBanner); - const nativeRequest = buildRequest(defaultBidRequestNative); + let request, nativeRequest; + before(() => { + request = buildRequest(defaultBidRequestBanner); + nativeRequest = buildRequest(defaultBidRequestNative); + }) it('sends bid request to endpoint via https using post', () => { expect(request.method).to.equal('POST'); @@ -176,34 +221,20 @@ describe('orbidderBidAdapter', () => { // we add two, because we add pageUrl and version from bidderRequest object expect(Object.keys(request.data).length).to.equal(Object.keys(defaultBidRequestBanner).length + 2); - expect(request.data.bidId).to.equal(defaultBidRequestBanner.bidId); - expect(request.data.auctionId).to.equal(defaultBidRequestBanner.auctionId); - expect(request.data.transactionId).to.equal(defaultBidRequestBanner.ortb2Imp.ext.tid); - expect(request.data.bidRequestCount).to.equal(defaultBidRequestBanner.bidRequestCount); - expect(request.data.adUnitCode).to.equal(defaultBidRequestBanner.adUnitCode); - expect(request.data.pageUrl).to.equal('https://localhost:9876/'); - expect(request.data.v).to.equal($$PREBID_GLOBAL$$.version); - expect(request.data.sizes).to.equal(defaultBidRequestBanner.sizes); - - expect(_.isEqual(request.data.params, defaultBidRequestBanner.params)).to.be.true; - expect(_.isEqual(request.data.mediaTypes, defaultBidRequestBanner.mediaTypes)).to.be.true; + const expectedBidRequest = deepClone(defaultBidRequestBanner); + expectedBidRequest.pageUrl = 'https://localhost:9876/'; + expectedBidRequest.v = $$PREBID_GLOBAL$$.version; + expect(request.data).to.deep.equal(expectedBidRequest); }); it('native: sends correct bid parameters', () => { // we add two, because we add pageUrl and version from bidderRequest object expect(Object.keys(nativeRequest.data).length).to.equal(Object.keys(defaultBidRequestNative).length + 2); - expect(nativeRequest.data.bidId).to.equal(defaultBidRequestNative.bidId); - expect(nativeRequest.data.auctionId).to.equal(defaultBidRequestNative.auctionId); - expect(nativeRequest.data.transactionId).to.equal(defaultBidRequestNative.ortb2Imp.ext.tid); - expect(nativeRequest.data.bidRequestCount).to.equal(defaultBidRequestNative.bidRequestCount); - expect(nativeRequest.data.adUnitCode).to.equal(defaultBidRequestNative.adUnitCode); - expect(nativeRequest.data.pageUrl).to.equal('https://localhost:9876/'); - expect(nativeRequest.data.v).to.equal($$PREBID_GLOBAL$$.version); - expect(nativeRequest.data.sizes).to.be.empty; - - expect(_.isEqual(nativeRequest.data.params, defaultBidRequestNative.params)).to.be.true; - expect(_.isEqual(nativeRequest.data.mediaTypes, defaultBidRequestNative.mediaTypes)).to.be.true; + const expectedBidRequest = deepClone(defaultBidRequestNative); + expectedBidRequest.pageUrl = 'https://localhost:9876/'; + expectedBidRequest.v = $$PREBID_GLOBAL$$.version; + expect(nativeRequest.data).to.deep.equal(expectedBidRequest); }); it('banner: handles empty gdpr object', () => { @@ -291,7 +322,7 @@ describe('orbidderBidAdapter', () => { }); }); - describe('buildRequests with price floor module', () => { + it('buildRequests with price floor module', () => { const bidRequest = deepClone(defaultBidRequestBanner); bidRequest.params.bidfloor = 1; bidRequest.getFloor = (floorObj) => { @@ -344,7 +375,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); expect(_.isEqual(expectedResponse, serverResponse)).to.be.true; }); @@ -384,7 +415,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); Object.keys(expectedResponse[0]).forEach((key) => { @@ -451,7 +482,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); expect(_.isEqual(expectedResponse, serverResponse)).to.be.true; @@ -471,7 +502,7 @@ describe('orbidderBidAdapter', () => { 'netRevenue': true, } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); @@ -489,7 +520,7 @@ describe('orbidderBidAdapter', () => { 'creativeId': '29681110', } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); @@ -515,13 +546,13 @@ describe('orbidderBidAdapter', () => { } } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); it('handles nobid responses', () => { const serverResponse = []; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); }); diff --git a/test/spec/modules/outbrainBidAdapter_spec.js b/test/spec/modules/outbrainBidAdapter_spec.js index ab72ff85ab0..e6abb5e9caa 100644 --- a/test/spec/modules/outbrainBidAdapter_spec.js +++ b/test/spec/modules/outbrainBidAdapter_spec.js @@ -1,9 +1,7 @@ import { expect } from 'chai'; -import { spec } from 'modules/outbrainBidAdapter.js'; +import { spec, storage } from 'modules/outbrainBidAdapter.js'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr'; -import { createEidsArray } from 'modules/userId/eids.js'; -import * as utils from 'src/utils.js'; describe('Outbrain Adapter', function () { describe('Bid request and response', function () { @@ -215,15 +213,18 @@ describe('Outbrain Adapter', function () { }) describe('buildRequests', function () { + let getDataFromLocalStorageStub; + before(() => { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage') config.setConfig({ outbrain: { bidderUrl: 'https://bidder-url.com', } - } - ) + }) }) after(() => { + getDataFromLocalStorageStub.restore() config.resetConfig() }) @@ -426,7 +427,7 @@ describe('Outbrain Adapter', function () { expect(resData.badv).to.deep.equal(['bad-advertiser']) }); - it('first party data', function () { + it('should pass first party data', function () { const bidRequest = { ...commonBidRequest, ...nativeBidRequestParams, @@ -507,13 +508,35 @@ describe('Outbrain Adapter', function () { config.resetConfig() }); + it('should pass gpp information', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + }; + const bidderRequest = { + ...commonBidderRequest, + 'gppConsent': { + 'gppString': 'abc12345', + 'applicableSections': [8] + } + } + + const res = spec.buildRequests([bidRequest], bidderRequest); + const resData = JSON.parse(res.data); + + expect(resData.regs.ext.gpp).to.exist; + expect(resData.regs.ext.gpp_sid).to.exist; + expect(resData.regs.ext.gpp).to.equal('abc12345'); + expect(resData.regs.ext.gpp_sid).to.deep.equal([8]); + }); + it('should pass extended ids', function () { let bidRequest = { bidId: 'bidId', params: {}, - userIdAsEids: createEidsArray({ - idl_env: 'id-value', - }), + userIdAsEids: [ + { source: 'liveramp.com', uids: [{ id: 'id-value', atype: 3 }] } + ], ...commonBidRequest, }; @@ -524,6 +547,22 @@ describe('Outbrain Adapter', function () { ]); }); + it('should pass OB user token', function () { + getDataFromLocalStorageStub.returns('12345'); + + let bidRequest = { + bidId: 'bidId', + params: {}, + ...commonBidRequest, + }; + + let res = spec.buildRequests([bidRequest], commonBidderRequest); + const resData = JSON.parse(res.data) + expect(resData.user.ext.obusertoken).to.equal('12345') + expect(getDataFromLocalStorageStub.called).to.be.true; + sinon.assert.calledWith(getDataFromLocalStorageStub, 'OB-USER-TOKEN'); + }); + it('should pass bidfloor', function () { const bidRequest = { ...commonBidRequest, @@ -844,6 +883,12 @@ describe('Outbrain Adapter', function () { type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }]); }); + + it('should pass gpp consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '', { gppString: 'abc12345', applicableSections: [1, 2] })).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?gpp=abc12345&gpp_sid=1%2C2` + }]); + }); }) describe('onBidWon', function () { diff --git a/test/spec/modules/oxxionAnalyticsAdapter_spec.js b/test/spec/modules/oxxionAnalyticsAdapter_spec.js index 63e929d7160..9d06be24f68 100644 --- a/test/spec/modules/oxxionAnalyticsAdapter_spec.js +++ b/test/spec/modules/oxxionAnalyticsAdapter_spec.js @@ -1,4 +1,5 @@ import oxxionAnalytics from 'modules/oxxionAnalyticsAdapter.js'; +import {dereferenceWithoutRenderer} from 'modules/oxxionAnalyticsAdapter.js'; import { expect } from 'chai'; import { server } from 'test/mocks/xhr.js'; let adapterManager = require('src/adapterManager').default; @@ -85,20 +86,21 @@ describe('Oxxion Analytics', function () { } }, 'adUnitCode': 'tag_200124_banner', - 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b40', + 'transactionId': '8b2a8629-d1ea-4bb1-aff0-e335b96dd002', 'sizes': [ [ 300, 600 ] ], - 'bidId': '34a63e5d5378a3', + 'bidId': '2bd3e8ff8a113f', 'bidderRequestId': '11dc6ff6378de7', 'auctionId': '1e8b993d-8f0a-4232-83eb-3639ddf3a44b', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 + 'bidderWinsCount': 0, + 'ova': 'cleared' } ], 'auctionStart': 1647424261187, @@ -148,14 +150,14 @@ describe('Oxxion Analytics', function () { 'bidsReceived': [ { 'bidderCode': 'appnexus', - 'width': 300, - 'height': 600, + 'width': 970, + 'height': 250, 'statusMessage': 'Bid available', - 'adId': '7a4ced80f33d33', - 'requestId': '34a63e5d5378a3', - 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b40', + 'adId': '65d16ef039a97a', + 'requestId': '2bd3e8ff8a113f', + 'transactionId': '8b2a8629-d1ea-4bb1-aff0-e335b96dd002', 'auctionId': '1e8b993d-8f0a-4232-83eb-3639ddf3a44b', - 'mediaType': 'banner', + 'mediaType': 'video', 'source': 'client', 'cpm': 27.4276, 'creativeId': '158534630', @@ -166,8 +168,10 @@ describe('Oxxion Analytics', function () { 'meta': { 'advertiserDomains': [ 'example.com' - ] + ], + 'demandSource': 'something' }, + 'renderer': 'something', 'originalCpm': 25.02521, 'originalCurrency': 'EUR', 'responseTimestamp': 1647424261559, @@ -184,7 +188,7 @@ describe('Oxxion Analytics', function () { 'size': '300x600', 'adserverTargeting': { 'hb_bidder': 'appnexus', - 'hb_adid': '7a4ced80f33d33', + 'hb_adid': '65d16ef039a97a', 'hb_pb': '20.000000', 'hb_size': '300x600', 'hb_source': 'client', @@ -221,6 +225,7 @@ describe('Oxxion Analytics', function () { 'example.com' ] }, + 'renderer': 'something', 'originalCpm': 25.02521, 'originalCurrency': 'EUR', 'responseTimestamp': 1647424261558, @@ -267,7 +272,24 @@ describe('Oxxion Analytics', function () { oxxionAnalytics.disableAnalytics(); oxxionAnalytics.track.restore(); }); + it('test dereferenceWithoutRenderer', function () { + adapterManager.registerAnalyticsAdapter({ + code: 'oxxion', + adapter: oxxionAnalytics + }); + adapterManager.enableAnalytics({ + provider: 'oxxion', + options: { + domain: 'test' + } + }); + let resultBidWon = JSON.parse(dereferenceWithoutRenderer(bidWon)); + expect(resultBidWon).not.to.have.property('renderer'); + let resultBid = JSON.parse(dereferenceWithoutRenderer(auctionEnd)); + expect(resultBid).to.have.property('bidsReceived').and.to.have.lengthOf(1); + expect(resultBid.bidsReceived[0]).not.to.have.property('renderer'); + }); it('test auctionEnd', function () { adapterManager.registerAnalyticsAdapter({ code: 'oxxion', @@ -293,14 +315,16 @@ describe('Oxxion Analytics', function () { expect(message.auctionEnd[0].bidsReceived[0]).not.to.have.property('ad'); expect(message.auctionEnd[0].bidsReceived[0]).to.have.property('meta'); expect(message.auctionEnd[0].bidsReceived[0].meta).to.have.property('advertiserDomains'); + expect(message.auctionEnd[0].bidsReceived[0].meta).to.have.property('demandSource'); expect(message.auctionEnd[0].bidsReceived[0]).to.have.property('adId'); expect(message.auctionEnd[0]).to.have.property('bidderRequests').and.to.have.lengthOf(1); expect(message.auctionEnd[0].bidderRequests[0]).to.have.property('gdprConsent'); expect(message.auctionEnd[0].bidderRequests[0].gdprConsent).not.to.have.property('vendorData'); - sinon.assert.callCount(oxxionAnalytics.track, 5); + expect(message.auctionEnd[0].bidderRequests[0]).to.have.property('oxxionMode'); }); it('test bidWon', function() { + window.OXXION_MODE = {'abtest': true}; adapterManager.registerAnalyticsAdapter({ code: 'oxxion', adapter: oxxionAnalytics @@ -318,7 +342,8 @@ describe('Oxxion Analytics', function () { expect(message).not.to.have.property('ad'); expect(message).to.have.property('adId') expect(message).to.have.property('cpmIncrement').and.to.equal(27.4276); - // sinon.assert.callCount(oxxionAnalytics.track, 1); + expect(message).to.have.property('oxxionMode').and.to.have.property('abtest').and.to.equal(true); + expect(message).to.have.property('ova').and.to.equal('cleared'); }); }); }); diff --git a/test/spec/modules/oxxionRtdProvider_spec.js b/test/spec/modules/oxxionRtdProvider_spec.js index 5bd8756bb6f..2a8024f3565 100644 --- a/test/spec/modules/oxxionRtdProvider_spec.js +++ b/test/spec/modules/oxxionRtdProvider_spec.js @@ -6,7 +6,10 @@ const utils = require('src/utils.js'); const moduleConfig = { params: { domain: 'test.endpoint', - contexts: ['instream', 'outstream'] + contexts: ['instream', 'outstream'], + samplingRate: 10, + threshold: false, + bidders: ['appnexus', 'mediasquare'], } }; @@ -31,6 +34,14 @@ let request = { ], 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b41' }, + { + 'code': 'msq_tag_200125_banner', + 'mediaTypes': { 'banner': { 'sizes': [[300, 250]] } }, + 'bids': [ + {'bidder': 'appnexusAst', 'params': {'placementId': 345678}}, + ], + 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b41' + } ] }; @@ -102,6 +113,22 @@ let bids = [{ }, ]; +let bidInterests = [ + {'id': 0, 'rate': 50.0, 'suggestion': true}, + {'id': 1, 'rate': 12.0, 'suggestion': false}, + {'id': 2, 'rate': 0.0, 'suggestion': true}, + {'id': 3, 'rate': 0.0, 'suggestion': false}, +]; + +const userConsent = { + 'gdpr': { + 'consentString': 'consent_hash' + }, + 'usp': null, + 'gpp': null, + 'coppa': false +}; + describe('oxxionRtdProvider', () => { describe('Oxxion RTD sub module', () => { it('should init, return true, and set the params', () => { @@ -114,30 +141,20 @@ describe('oxxionRtdProvider', () => { auctionEnd.bidsReceived = bids; it('call everything', function() { oxxionSubmodule.getBidRequestData(request, null, moduleConfig); - oxxionSubmodule.onBidResponseEvent(auctionEnd.bidsReceived[0], moduleConfig); - oxxionSubmodule.onBidResponseEvent(auctionEnd.bidsReceived[1], moduleConfig); - }); - it('check vastImpUrl', function() { - expect(auctionEnd.bidsReceived[0]).to.have.property('vastImpUrl'); - let expectVastImpUrl = 'https://' + moduleConfig.params.domain + '.oxxion.io/analytics/vast_imp?'; - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(expectVastImpUrl); - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(encodeURI('https://some.tracking-url.com')); - }); - it('check vastXml', function() { - expect(auctionEnd.bidsReceived[0]).to.have.property('vastXml'); - let vastWrapper = new DOMParser().parseFromString(auctionEnd.bidsReceived[0].vastXml, 'text/xml'); - let impressions = vastWrapper.querySelectorAll('VAST Ad Wrapper Impression'); - expect(impressions.length).to.equal(2); - expect(auctionEnd.bidsReceived[1]).to.have.property('vastXml'); - expect(auctionEnd.bidsReceived[1].adId).to.equal('4b2e1581c0ca1a'); - let vastInline = new DOMParser().parseFromString(auctionEnd.bidsReceived[1].vastXml, 'text/xml'); - let inline = vastInline.querySelectorAll('VAST Ad InLine'); - expect(inline).to.have.lengthOf(1); - let inlineImpressions = vastInline.querySelectorAll('VAST Ad InLine Impression'); - expect(inlineImpressions).to.have.lengthOf.above(0); }); - it('check cpmIncrement', function() { - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(encodeURI('cpmIncrement=0')); + it('check bid filtering', function() { + let requestsList = oxxionSubmodule.getRequestsList(request); + expect(requestsList.length).to.equal(4); + expect(requestsList[0]).to.have.property('id'); + expect(request.adUnits[0].bids[0]).to.have.property('_id'); + expect(requestsList[0].id).to.equal(request.adUnits[0].bids[0]._id); + const [filteredBiddderRequests, filteredBids] = oxxionSubmodule.getFilteredAdUnitsOnBidRates(bidInterests, request.adUnits, moduleConfig.params, false); + expect(filteredBids.length).to.equal(1); + expect(filteredBiddderRequests.length).to.equal(3); + expect(filteredBiddderRequests[0]).to.have.property('bids'); + expect(filteredBiddderRequests[0].bids.length).to.equal(1); + expect(filteredBiddderRequests[1]).to.have.property('bids'); + expect(filteredBiddderRequests[1].bids.length).to.equal(1); }); }); }); diff --git a/test/spec/modules/ozoneBidAdapter_spec.js b/test/spec/modules/ozoneBidAdapter_spec.js index 4c7e330b237..73df2fba8fd 100644 --- a/test/spec/modules/ozoneBidAdapter_spec.js +++ b/test/spec/modules/ozoneBidAdapter_spec.js @@ -6,7 +6,6 @@ import {getGranularityKeyName, getGranularityObject} from '../../../modules/ozon import * as utils from '../../../src/utils.js'; const OZONEURI = 'https://elb.the-ozone-project.com/openrtb2/auction'; const BIDDER_CODE = 'ozone'; - var validBidRequests = [ { adUnitCode: 'div-gpt-ad-1460505748561-0', @@ -2049,6 +2048,40 @@ describe('ozone Adapter', function () { 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'); }); + it('should pass gpid to auction if it is present (gptPreAuction adapter sets this)', function () { + var specMock = utils.deepClone(spec); + let br = JSON.parse(JSON.stringify(validBidRequests)); + utils.deepSetValue(br[0], 'ortb2Imp.ext.gpid', '/22037345/projectozone'); + const request = specMock.buildRequests(br, validBidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].ext.gpid).to.equal('/22037345/projectozone'); + }); + it('should batch into 10s if config is set', function () { + config.setConfig({ozone: {'batchRequests': true}}); + var specMock = utils.deepClone(spec); + let arrReq = []; + for (let i = 0; i < 25; i++) { + let b = validBidRequests[0]; + b.adUnitCode += i; + arrReq.push(b); + } + const request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.length).to.equal(3); + config.resetConfig(); + }); + it('should not batch into 10s if config is set to false and singleRequest is true', function () { + config.setConfig({ozone: {'batchRequests': false, 'singleRequest': true}}); + var specMock = utils.deepClone(spec); + let arrReq = []; + for (let i = 0; i < 15; i++) { + let b = validBidRequests[0]; + b.adUnitCode += i; + arrReq.push(b); + } + const request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.method).to.equal('POST'); + config.resetConfig(); + }); it('should use GET values auction=dev & cookiesync=dev if set', function() { var specMock = utils.deepClone(spec); specMock.getGetParametersAsObject = function() { @@ -2429,6 +2462,12 @@ describe('ozone Adapter', function () { const result = spec.interpretResponse(validres, request); expect(utils.deepAccess(result[0].adserverTargeting, 'oz_ozappnexus_sid')).to.equal(result[0].cid); }); + it('should add oz_auc_id (response id value)', function () { + const request = spec.buildRequests(validBidRequestsMulti, validBidderRequest); + let validres = JSON.parse(JSON.stringify(validBidResponse1adWith2Bidders)); + const result = spec.interpretResponse(validres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_auc_id')).to.equal(validBidResponse1adWith2Bidders.body.id); + }); it('should add unique adId values to each bid', function() { const request = spec.buildRequests(validBidRequests, validBidderRequest); let validres = JSON.parse(JSON.stringify(validResponse2BidsSameAdunit)); diff --git a/test/spec/modules/pangleBidAdapter_spec.js b/test/spec/modules/pangleBidAdapter_spec.js new file mode 100644 index 00000000000..94f5a6e4b1d --- /dev/null +++ b/test/spec/modules/pangleBidAdapter_spec.js @@ -0,0 +1,295 @@ +import { expect } from 'chai'; +import { spec } from 'modules/pangleBidAdapter.js'; +import { logInfo } from '../../../src/utils'; + +const REQUEST = [{ + adUnitCode: 'adUnitCode1', + bidId: 'bidId1', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + ortb2Imp: { + ext: { + tid: 'cccc1234', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'pangle', + params: { + placementid: 999, + appid: 111, + }, +}, +{ + adUnitCode: 'adUnitCode2', + bidId: 'bidId2', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + ortb2Imp: { + ext: { + tid: 'cccc1234', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'pangle', + params: { + placementid: 999, + appid: 111, + }, +}]; + +const DEFAULT_OPTIONS = { + userId: { + britepoolid: 'pangle-britepool', + criteoId: 'pangle-criteo', + digitrustid: { data: { id: 'pangle-digitrust' } }, + id5id: { uid: 'pangle-id5' }, + idl_env: 'pangle-idl-env', + lipb: { lipbid: 'pangle-liveintent' }, + netId: 'pangle-netid', + parrableId: { eid: 'pangle-parrable' }, + pubcid: 'pangle-pubcid', + tdid: 'pangle-ttd', + } +}; + +const RESPONSE = { + 'headers': null, + 'body': { + 'id': 'requestId', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'bidId1', + 'impid': 'bidId1', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'pangle': { + 'brand_id': 334553, + 'auction_id': 514667951122925701, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + } + ], + 'seat': 'seat' + } + ] + } +}; + +describe('pangle bid adapter', function () { + describe('isBidRequestValid', function () { + it('should accept request if placementid and appid is passed', function () { + let bid = { + bidder: 'pangle', + params: { + token: 'xxx', + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('reject requests without params', function () { + let bid = { + bidder: 'pangle', + params: {} + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('creates request data', function () { + let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', REQUEST[0].bidId); + expect(payload.imp[1]).to.have.property('id', REQUEST[1].bidId); + }); + }); + + describe('interpretResponse', function () { + it('has bids', function () { + let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; + let bids = spec.interpretResponse(RESPONSE, request); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].id); + expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); + expect(bids[index]).to.have.property('ttl', 30); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + + it('handles empty response', function () { + let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; + const EMPTY_RESP = Object.assign({}, RESPONSE, { 'body': {} }); + const bids = spec.interpretResponse(EMPTY_RESP, request); + expect(bids).to.be.empty; + }); + }); + + describe('parseUserAgent', function () { + let desktop, mobile, tablet; + beforeEach(function () { + desktop = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'; + mobile = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'; + tablet = 'Apple iPad: Mozilla/5.0 (iPad; CPU OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/605.1.15'; + }); + + it('should return correct device type: tablet', function () { + let deviceType = spec.getDeviceType(tablet); + expect(deviceType).to.equal(5); + }); + + it('should return correct device type: mobile', function () { + let deviceType = spec.getDeviceType(mobile); + expect(deviceType).to.equal(4); + }); + + it('should return correct device type: desktop', function () { + let deviceType = spec.getDeviceType(desktop); + expect(deviceType).to.equal(2); + }); + }); + + // describe('video', function () { + // it('video config', function() { + // logInfo(spec.buildRequests(VIDEO_REQUEST, DEFAULT_OPTIONS)[0].data, 'spec.buildRequests(videoConfig, DEFAULT_OPTIONS)[0].data.imp[0]'); + // const request = spec.buildRequests(VIDEO_REQUEST, DEFAULT_OPTIONS)[0]; + + // expect(request).to.exist.and.to.be.a('object'); + // const payload = request.data; + // expect(payload).to.exist.and.to.be.a('object'); + // const video = payload.imp[0].video; + // expect(video).to.exist.and.to.be.a('object'); + // // console.log(video, 'video???') + // // expect(url).to.equal('https://pangle.pangleglobal.com/api/ad/union/web_js/common/get_ads'); + // // assert.deepEqual(video, { + // // h: 380, + // // mimes: ['video/mp4'], + // // w: 240 + // // }) + // }) + // }) +}); + +describe('Pangle Adapter with video', function() { + const videoBidRequest = [ + { + bidId: '2820132fe18114', + mediaTypes: { video: { context: 'outstream', playerSize: [[300, 250]] } }, + params: { token: 'test-token' } + } + ]; + const bidderRequest = { + refererInfo: { + referer: 'https://example.com' + } + }; + const serverResponse = { + 'headers': null, + 'body': { + 'id': '233f1693-68d1-470a-ad85-c156c3faaf6f', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2820132fe18114', + 'impid': '2820132fe18114', + 'price': 0.03294, + 'nurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/win/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&win_price=${AUCTION_PRICE}&auction_mwb=${AUCTION_BID_TO_WIN}&use_pb=1', + 'lurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/loss/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&reason=${AUCTION_LOSS}&ad_slot_type=8&auction_mwb=${AUCTION_PRICE}&use_pb=1', + 'adm': '', + 'adid': '1780626232977441', + 'adomain': [ + 'swi.esxcmnb.com' + ], + 'iurl': 'https://p16-ttam-va.ibyteimg.com/origin/ad-site-i18n-sg/202310245d0d598b3ff5993c4f129a8b', + 'cid': '1780626232977441', + 'crid': '1780626232977441', + 'attr': [ + 4 + ], + 'w': 640, + 'h': 640, + 'ext': { + 'pangle': { + 'adtype': 8 + }, + 'event_notification_token': { + 'payload': '980589944:8:1450:7492' + } + } + } + ], + 'seat': 'pangle' + } + ] + } + }; + + describe('Video: buildRequests', function() { + it('should create a POST request for video bid', function() { + const requests = spec.buildRequests(videoBidRequest, bidderRequest); + expect(requests[0].method).to.equal('POST'); + }); + + it('should have a valid URL and payload for an out-stream video bid', function () { + const requests = spec.buildRequests(videoBidRequest, bidderRequest); + expect(requests[0].url).to.equal('https://pangle.pangleglobal.com/api/ad/union/web_js/common/get_ads'); + expect(requests[0].data).to.exist; + }); + }); + + describe('interpretResponse: Video', function () { + it('should get correct bid response', function () { + const request = spec.buildRequests(videoBidRequest, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(serverResponse, request); + expect(interpretedResponse).to.be.an('array'); + const bid = interpretedResponse[0]; + expect(bid).to.exist; + expect(bid.requestId).to.exist; + expect(bid.cpm).to.be.above(0); + expect(bid.ttl).to.exist; + expect(bid.creativeId).to.exist; + if (bid.renderer) { + expect(bid.renderer.render).to.exist; + } + }); + }); +}); diff --git a/test/spec/modules/acuityAdsBidAdapter_spec.js b/test/spec/modules/pgamsspBidAdapter_spec.js similarity index 96% rename from test/spec/modules/acuityAdsBidAdapter_spec.js rename to test/spec/modules/pgamsspBidAdapter_spec.js index 05c59036ff3..7e2323d4b81 100644 --- a/test/spec/modules/acuityAdsBidAdapter_spec.js +++ b/test/spec/modules/pgamsspBidAdapter_spec.js @@ -1,11 +1,11 @@ import { expect } from 'chai'; -import { spec } from '../../../modules/acuityAdsBidAdapter'; +import { spec } from '../../../modules/pgamsspBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'acuityads' +const bidder = 'pgamssp' -describe('AcuityAdsBidAdapter', function () { +describe('PGAMBidAdapter', function () { const bids = [ { bidId: getUniqueIdentifierStr(), @@ -104,7 +104,7 @@ describe('AcuityAdsBidAdapter', function () { }); it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://prebid.admanmedia.com/pbjs'); + expect(serverRequest.url).to.equal('https://us-east.pgammedia.com/pbjs'); }); it('Returns general data valid', function () { @@ -382,7 +382,7 @@ describe('AcuityAdsBidAdapter', function () { expect(syncData[0].type).to.be.a('string') expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + expect(syncData[0].url).to.equal('https://cs.pgammedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') }); it('Should return array of objects with proper sync config , include CCPA', function() { const syncData = spec.getUserSyncs({}, {}, {}, { @@ -393,7 +393,7 @@ describe('AcuityAdsBidAdapter', function () { expect(syncData[0].type).to.be.a('string') expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + expect(syncData[0].url).to.equal('https://cs.pgammedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') }); }); }); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 895c3754876..5b591804b95 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -6,7 +6,7 @@ import { resetWurlMap, s2sDefaultConfig } from 'modules/prebidServerBidAdapter/index.js'; -import adapterManager from 'src/adapterManager.js'; +import adapterManager, {PBS_ADAPTER_NAME} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import {deepAccess, deepClone, mergeDeep} from 'src/utils.js'; import {ajax} from 'src/ajax.js'; @@ -14,7 +14,6 @@ import {config} from 'src/config.js'; import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import {server} from 'test/mocks/xhr.js'; -import {createEidsArray} from 'modules/userId/eids.js'; import 'modules/appnexusBidAdapter.js'; // appnexus alias test import 'modules/rubiconBidAdapter.js'; // rubicon alias test import 'src/prebid.js'; // $$PREBID_GLOBAL$$.aliasBidder test @@ -27,14 +26,17 @@ import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; import 'modules/fledgeForGpt.js'; import * as redactor from 'src/activities/redactor.js'; +import * as activityRules from 'src/activities/rules.js'; import {hook} from '../../../src/hook.js'; import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; import {auctionManager} from '../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {addComponentAuction, registerBidder} from 'src/adapters/bidderFactory.js'; import {getGlobal} from '../../../src/prebidGlobal.js'; -import {syncAddFPDEnrichments, syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; import {deepSetValue} from '../../../src/utils.js'; +import {ACTIVITY_TRANSMIT_UFPD} from '../../../src/activities/activities.js'; +import {MODULE_TYPE_PREBID} from '../../../src/activities/modules.js'; let CONFIG = { accountId: '1', @@ -707,8 +709,8 @@ describe('S2S Adapter', function () { beforeEach(() => { s2sReq = { ...REQUEST, - ortb2Fragments: {global: {source: {tid: 'mock-tid'}}}, - ad_units: REQUEST.ad_units.map(au => ({...au, ortb2Imp: {ext: {tid: 'mock-tid'}}})) + ortb2Fragments: {global: {}}, + ad_units: REQUEST.ad_units.map(au => ({...au, ortb2Imp: {ext: {tid: 'mock-tid'}}})), }; BID_REQUESTS[0].bids[0].ortb2Imp = {ext: {tid: 'mock-tid'}}; }); @@ -721,26 +723,52 @@ describe('S2S Adapter', function () { it('should not be set when transmitTid is not allowed, with ext.prebid.createtids: false', () => { config.setConfig({ s2sConfig: CONFIG, enableTIDs: false }); const req = makeRequest(); - expect(req.source.tid).to.not.exist; - expect(req.imp[0].ext.tid).to.not.exist; + expect(req.source?.tid).to.not.exist; + expect(req.imp[0].ext?.tid).to.not.exist; expect(req.ext.prebid.createtids).to.equal(false); }); - it('should be picked from FPD otherwise', () => { + it('should be set to auction ID otherwise', () => { config.setConfig({s2sConfig: CONFIG, enableTIDs: true}); const req = makeRequest(); - expect(req.source.tid).to.eql('mock-tid'); + expect(req.source.tid).to.eql(BID_REQUESTS[0].auctionId); expect(req.imp[0].ext.tid).to.eql('mock-tid'); }) }) + describe('browsingTopics', () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore() + }); + Object.entries({ + 'allowed': true, + 'not allowed': false, + }).forEach(([t, allow]) => { + it(`should be set to ${allow} when transmitUfpd is ${t}`, () => { + sandbox.stub(activityRules, 'isActivityAllowed').callsFake((activity, params) => { + if (activity === ACTIVITY_TRANSMIT_UFPD && params.component === `${MODULE_TYPE_PREBID}.${PBS_ADAPTER_NAME}`) { + return allow; + } + return false; + }); + config.setConfig({s2sConfig: CONFIG}); + const ajax = sinon.stub(); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + sinon.assert.calledWith(ajax, sinon.match.any, sinon.match.any, sinon.match.any, sinon.match({ + browsingTopics: allow + })); + }); + }); + }) + it('should set tmax to s2sConfig.timeout', () => { const cfg = {...CONFIG, timeout: 123}; config.setConfig({s2sConfig: cfg}); adapter.callBids({...REQUEST, s2sConfig: cfg}, BID_REQUESTS, addBidResponse, done, ajax); const req = JSON.parse(server.requests[0].requestBody); expect(req.tmax).to.eql(123); - }) + }); it('should block request if config did not define p1Consent URL in endpoint object config', function () { let badConfig = utils.deepClone(CONFIG); @@ -848,7 +876,7 @@ describe('S2S Adapter', function () { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); - function mockConsent({applies = true, hasP1Consent = true} = {}) { + function mockTCF({applies = true, hasP1Consent = true} = {}) { return { consentString: 'mockConsent', gdprApplies: applies, @@ -866,7 +894,7 @@ describe('S2S Adapter', function () { config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent(); + gdprBidRequest[0].gdprConsent = mockTCF(); adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, gdprBidRequest), gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); @@ -889,7 +917,7 @@ describe('S2S Adapter', function () { config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = Object.assign(mockConsent(), { + gdprBidRequest[0].gdprConsent = Object.assign(mockTCF(), { addtlConsent: 'superduperconsent', }); @@ -909,69 +937,6 @@ describe('S2S Adapter', function () { expect(requestBid.regs).to.not.exist; expect(requestBid.user).to.not.exist; }); - - it('check gdpr info gets added into cookie_sync request: have consent data', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - - gdprBidRequest[0].gdprConsent = mockConsent(); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(1); - expect(requestBid.gdpr_consent).is.equal('mockConsent'); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - }); - - it('check gdpr info gets added into cookie_sync request: have consent data but gdprApplies is false', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent({applies: false}); - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(0); - expect(requestBid.gdpr_consent).is.undefined; - }); - - it('checks gdpr info gets added to cookie_sync request: applies is false', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent({applies: false}); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(0); - expect(requestBid.gdpr_consent).is.undefined; - }); }); describe('us_privacy (ccpa) consent data', function () { @@ -998,25 +963,6 @@ describe('S2S Adapter', function () { expect(requestBid.regs).to.not.exist; }); - - it('is added to cookie_sync request when in bidRequest', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - config.setConfig({ s2sConfig: cookieSyncConfig }); - - let uspBidRequest = utils.deepClone(BID_REQUESTS); - uspBidRequest[0].uspConsent = '1YNN'; - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, uspBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.us_privacy).is.equal('1YNN'); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - }); }); describe('gdpr and us_privacy (ccpa) consent data', function () { @@ -1029,7 +975,7 @@ describe('S2S Adapter', function () { let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1NYN'; - consentBidRequest[0].gdprConsent = mockConsent(); + consentBidRequest[0].gdprConsent = mockTCF(); adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, consentBidRequest), consentBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); @@ -1055,7 +1001,7 @@ describe('S2S Adapter', function () { let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1YNN'; - consentBidRequest[0].gdprConsent = mockConsent(); + consentBidRequest[0].gdprConsent = mockTCF(); const s2sBidRequest = utils.deepClone(REQUEST); s2sBidRequest.s2sConfig = cookieSyncConfig @@ -1812,170 +1758,177 @@ describe('S2S Adapter', function () { }]); }); - describe('filterSettings', function () { - const getRequestBid = userSync => { - let cookieSyncConfig = utils.deepClone(CONFIG); - const s2sBidRequest = utils.deepClone(REQUEST); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - s2sBidRequest.s2sConfig = cookieSyncConfig; + describe('cookie sync', () => { + let s2sConfig, bidderReqs; - config.setConfig({ userSync, s2sConfig: cookieSyncConfig }); + beforeEach(() => { + bidderReqs = utils.deepClone(BID_REQUESTS); + s2sConfig = utils.deepClone(CONFIG); + s2sConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; + }) - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); + function callCookieSync() { + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + config.setConfig({ s2sConfig: s2sConfig }); + adapter.callBids(s2sBidRequest, bidderReqs, addBidResponse, done, ajax); return JSON.parse(server.requests[0].requestBody); } - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the all key is present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - all: { - bidders: ['appnexus', 'rubicon', 'pubmatic'], - filter: 'exclude' + describe('filterSettings', function () { + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the all key is present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + all: { + bidders: ['appnexus', 'rubicon', 'pubmatic'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); - - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['appnexus', 'rubicon', 'pubmatic'], - 'filter': 'exclude' - }, - 'iframe': { - 'bidders': ['appnexus', 'rubicon', 'pubmatic'], - 'filter': 'exclude' - } + }); + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['appnexus', 'rubicon', 'pubmatic'], + 'filter': 'exclude' + }, + 'iframe': { + 'bidders': ['appnexus', 'rubicon', 'pubmatic'], + 'filter': 'exclude' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the iframe key is present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - iframe: { - bidders: ['rubicon', 'pubmatic'], - filter: 'include' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the iframe key is present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: ['rubicon', 'pubmatic'], + filter: 'include' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': '*', - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['rubicon', 'pubmatic'], - 'filter': 'include' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': '*', + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['rubicon', 'pubmatic'], + 'filter': 'include' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the image and iframe keys are both present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - image: { - bidders: ['triplelift', 'appnexus'], - filter: 'include' - }, - iframe: { - bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - filter: 'exclude' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the image and iframe keys are both present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + image: { + bidders: ['triplelift', 'appnexus'], + filter: 'include' + }, + iframe: { + bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['triplelift', 'appnexus'], - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - 'filter': 'exclude' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['triplelift', 'appnexus'], + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + 'filter': 'exclude' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the all and iframe keys are both present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - all: { - bidders: ['triplelift', 'appnexus'], - filter: 'include' - }, - iframe: { - bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - filter: 'exclude' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the all and iframe keys are both present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + all: { + bidders: ['triplelift', 'appnexus'], + filter: 'include' + }, + iframe: { + bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['triplelift', 'appnexus'], - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - 'filter': 'exclude' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['triplelift', 'appnexus'], + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + 'filter': 'exclude' + } + }); }); }); - }); - - it('adds limit to the cookie_sync request if userSyncLimit is greater than 0', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - cookieSyncConfig.userSyncLimit = 1; - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - config.setConfig({ s2sConfig: cookieSyncConfig }); - - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.equal(1); - }); - - it('does not add limit to cooke_sync request if userSyncLimit is missing or 0', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - config.setConfig({ s2sConfig: cookieSyncConfig }); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); + describe('limit', () => { + it('is added to request if userSyncLimit is greater than 0', function () { + s2sConfig.userSyncLimit = 1; + const req = callCookieSync(); + expect(req.limit).is.equal(1); + }); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.undefined; + Object.entries({ + 'missing': () => null, + '0': () => { s2sConfig.userSyncLimit = 0; } + }).forEach(([t, setup]) => { + it(`is not added to request if userSyncLimit is ${t}`, () => { + setup(); + const req = callCookieSync(); + expect(req.limit).to.not.exist; + }); + }); + }); - cookieSyncConfig.userSyncLimit = 0; - config.resetConfig(); - config.setConfig({ s2sConfig: cookieSyncConfig }); + describe('gdpr data is set', () => { + it('when we have consent data', function () { + bidderReqs[0].gdprConsent = mockTCF(); + const req = callCookieSync(); + expect(req.gdpr).is.equal(1); + expect(req.gdpr_consent).is.equal('mockConsent'); + }); - const s2sBidRequest2 = utils.deepClone(REQUEST); - s2sBidRequest2.s2sConfig = cookieSyncConfig; + it('when gdprApplies is false', () => { + bidderReqs[0].gdprConsent = mockTCF({applies: false}); + const req = callCookieSync(); + expect(req.gdpr).is.equal(0); + expect(req.gdpr_consent).is.undefined; + }); + }); - bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest2, bidRequest, addBidResponse, done, ajax); - requestBid = JSON.parse(server.requests[0].requestBody); + it('adds USP data from bidder request', () => { + bidderReqs[0].uspConsent = '1YNN'; + expect(callCookieSync().us_privacy).to.equal('1YNN'); + }); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.undefined; + it('adds GPP data from bidder requests', () => { + bidderReqs[0].gppConsent = { + applicableSections: [1, 2, 3], + gppString: 'mock-string' + }; + const req = callCookieSync(); + expect(req.gpp).to.eql('mock-string'); + expect(req.gpp_sid).to.eql('1,2,3'); + }); }); it('adds s2sConfig adapterOptions to request for ORTB', function () { @@ -2082,30 +2035,12 @@ describe('S2S Adapter', function () { } } }; - userIdBidRequest[0].bids[0].userIdAsEids = createEidsArray(userIdBidRequest[0].bids[0].userId); + userIdBidRequest[0].bids[0].userIdAsEids = [{id: 1}, {id: 2}]; adapter.callBids(REQUEST, userIdBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(typeof requestBid.user.ext.eids).is.equal('object'); - expect(Array.isArray(requestBid.user.ext.eids)).to.be.true; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'adserver.org')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'adserver.org')[0].uids[0].id).is.equal('abc123'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'criteo.com')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'criteo.com')[0].uids[0].id).is.equal('44VmRDeUE3ZGJ5MzRkRVJHU3BIUlJ6TlFPQUFU'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'pubcid.org')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'pubcid.org')[0].uids[0].id).is.equal('1234'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'parrable.com')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'parrable.com')[0].uids[0].id).is.equal('01.1563917337.test-eid'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].uids[0].id).is.equal('li-xyz'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments.length).is.equal(2); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments[0]).is.equal('segA'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments[1]).is.equal('segB'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')[0].uids[0].id).is.equal('11111'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')[0].uids[0].ext.linkType).is.equal('some-link-type'); - // LiveRamp should exist - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveramp.com')[0].uids[0].id).is.equal('0000-1111-2222-3333'); + expect(requestBid.user.ext.eids).to.eql([{id: 1}, {id: 2}]); }); it('when config \'currency.adServerCurrency\' value is a string: ORTB has property \'cur\' value set to a single item array', function () { @@ -2116,7 +2051,7 @@ describe('S2S Adapter', function () { const bidRequests = utils.deepClone(BID_REQUESTS); adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); - const parsedRequestBody = JSON.parse(server.requests[1].requestBody); + const parsedRequestBody = JSON.parse(server.requests.find(req => req.method === 'POST').requestBody); expect(parsedRequestBody.cur).to.deep.equal(['NZ']); }); @@ -2938,6 +2873,15 @@ describe('S2S Adapter', function () { events.emit.restore(); }); + it('triggers BIDDER_ERROR on server error', () => { + config.setConfig({ s2sConfig: CONFIG }); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(400, {}, {}); + BID_REQUESTS.forEach(bidderRequest => { + sinon.assert.calledWith(events.emit, CONSTANTS.EVENTS.BIDDER_ERROR, sinon.match({bidderRequest})) + }) + }) + // TODO: test dependent on pbjs_api_spec. Needs to be isolated it('does not call addBidResponse and calls done when ad unit not set', function () { config.setConfig({ s2sConfig: CONFIG }); @@ -3478,29 +3422,6 @@ describe('S2S Adapter', function () { }); }); describe('when the response contains ext.prebid.fledge', () => { - let fledgeStub, request, bidderRequests; - - function fledgeHook(next, ...args) { - fledgeStub(...args); - } - - before(() => { - addComponentAuction.before(fledgeHook); - }); - - after(() => { - addComponentAuction.getHooks({hook: fledgeHook}).remove(); - }) - - beforeEach(function () { - fledgeStub = sinon.stub(); - config.setConfig({CONFIG}); - request = deepClone(REQUEST); - request.ad_units.forEach(au => deepSetValue(au, 'ortb2Imp.ext.ae', 1)); - bidderRequests = deepClone(BID_REQUESTS); - bidderRequests.forEach(req => req.fledgeEnabled = true); - }); - const AU = 'div-gpt-ad-1460505748561-0'; const FLEDGE_RESP = { ext: { @@ -3509,12 +3430,14 @@ describe('S2S Adapter', function () { auctionconfigs: [ { impid: AU, + bidder: 'appnexus', config: { id: 1 } }, { impid: AU, + bidder: 'other', config: { id: 2 } @@ -3525,20 +3448,62 @@ describe('S2S Adapter', function () { } } + let fledgeStub, request, bidderRequests; + + function fledgeHook(next, ...args) { + fledgeStub(...args); + } + + before(() => { + addComponentAuction.before(fledgeHook); + }); + + after(() => { + addComponentAuction.getHooks({hook: fledgeHook}).remove(); + }) + + beforeEach(function () { + fledgeStub = sinon.stub(); + config.setConfig({CONFIG}); + bidderRequests = deepClone(BID_REQUESTS); + AU + bidderRequests.forEach(req => { + Object.assign(req, { + fledgeEnabled: true, + ortb2: { + fpd: 1 + } + }) + req.bids.forEach(bid => { + Object.assign(bid, { + ortb2Imp: { + fpd: 2 + } + }) + }) + }); + request = deepClone(REQUEST); + request.ad_units.forEach(au => deepSetValue(au, 'ortb2Imp.ext.ae', 1)); + }); + + function expectFledgeCalls() { + const auctionId = bidderRequests[0].auctionId; + sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: bidderRequests[0].ortb2, ortb2Imp: bidderRequests[0].bids[0].ortb2Imp}), {id: 1}) + sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: undefined, ortb2Imp: undefined}), {id: 2}) + } + it('calls addComponentAuction alongside addBidResponse', function () { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(mergeDeep({}, RESPONSE_OPENRTB, FLEDGE_RESP))); expect(addBidResponse.called).to.be.true; - sinon.assert.calledWith(fledgeStub, AU, {id: 1}); - sinon.assert.calledWith(fledgeStub, AU, {id: 2}); + expectFledgeCalls(); }); it('calls addComponentAuction when there is no bid in the response', () => { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(FLEDGE_RESP)); expect(addBidResponse.called).to.be.false; - sinon.assert.calledWith(fledgeStub, AU, {id: 1}); - sinon.assert.calledWith(fledgeStub, AU, {id: 2}); + expectFledgeCalls(); }) }); }); @@ -3688,33 +3653,6 @@ describe('S2S Adapter', function () { sinon.assert.calledOnce(logErrorSpy); }); - it('should configure the s2sConfig object with appnexus vendor defaults unless specified by user', function () { - const options = { - accountId: '123', - bidders: ['appnexus'], - defaultVendor: 'appnexus', - timeout: 750 - }; - - config.setConfig({ s2sConfig: options }); - sinon.assert.notCalled(logErrorSpy); - - let vendorConfig = config.getConfig('s2sConfig'); - expect(vendorConfig).to.have.property('accountId', '123'); - expect(vendorConfig).to.have.property('adapter', 'prebidServer'); - expect(vendorConfig.bidders).to.deep.equal(['appnexus']); - expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig.endpoint).to.deep.equal({ - p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction' - }); - expect(vendorConfig.syncEndpoint).to.deep.equal({ - p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' - }); - expect(vendorConfig).to.have.property('timeout', 750); - }); - it('should configure the s2sConfig object with appnexuspsp vendor defaults unless specified by user', function () { const options = { accountId: '123', @@ -3735,7 +3673,10 @@ describe('S2S Adapter', function () { p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' }); - expect(vendorConfig.syncEndpoint).to.be.undefined; + expect(vendorConfig.syncEndpoint).to.deep.equal({ + p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', + noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' + }); expect(vendorConfig).to.have.property('timeout', 750); }); @@ -3820,6 +3761,74 @@ describe('S2S Adapter', function () { }) }); + it('should configure the s2sConfig object with openwrap vendor defaults unless specified by user', function () { + const options = { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap' + }; + + config.setConfig({ s2sConfig: options }); + sinon.assert.notCalled(logErrorSpy); + + let vendorConfig = config.getConfig('s2sConfig'); + expect(vendorConfig).to.have.property('accountId', '1234'); + expect(vendorConfig).to.have.property('adapter', 'prebidServer'); + expect(vendorConfig.bidders).to.deep.equal(['pubmatic']); + expect(vendorConfig.enabled).to.be.true; + expect(vendorConfig.endpoint).to.deep.equal({ + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }); + expect(vendorConfig).to.have.property('timeout', 500); + }); + + it('should return proper defaults', function () { + const options = { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + timeout: 500 + }; + + config.setConfig({ s2sConfig: options }); + expect(config.getConfig('s2sConfig')).to.deep.equal({ + 'accountId': '1234', + 'adapter': 'prebidServer', + 'bidders': ['pubmatic'], + 'defaultVendor': 'openwrap', + 'enabled': true, + 'endpoint': { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + 'timeout': 500 + }) + }); + + it('should return default adapterOptions if not set', function () { + config.setConfig({ + s2sConfig: { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + timeout: 500 + } + }); + expect(config.getConfig('s2sConfig')).to.deep.equal({ + enabled: true, + timeout: 500, + adapter: 'prebidServer', + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + endpoint: { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + }) + }); + it('should set adapterOptions', function () { config.setConfig({ s2sConfig: { diff --git a/test/spec/modules/precisoBidAdapter_spec.js b/test/spec/modules/precisoBidAdapter_spec.js new file mode 100644 index 00000000000..78a1615a02e --- /dev/null +++ b/test/spec/modules/precisoBidAdapter_spec.js @@ -0,0 +1,162 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/precisoBidAdapter.js'; +import { config } from '../../../src/config.js'; + +const DEFAULT_PRICE = 1 +const DEFAULT_CURRENCY = 'USD' +const DEFAULT_BANNER_WIDTH = 300 +const DEFAULT_BANNER_HEIGHT = 250 +const BIDDER_CODE = 'preciso'; + +describe('PrecisoAdapter', function () { + let bid = { + bidId: '23fhj33i987f', + bidder: 'preciso', + mediaTypes: { + banner: { + sizes: [[DEFAULT_BANNER_WIDTH, DEFAULT_BANNER_HEIGHT]] + } + }, + params: { + host: 'prebid', + sourceid: '0', + publisherId: '0', + mediaType: 'banner', + region: 'prebid-eu' + + }, + userId: { + pubcid: '12355454test' + + }, + geo: 'NA', + city: 'Asia,delhi' + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and sourceid parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://ssp-bidder.mndtrk.com/bid_request/openrtb'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + // expect(data).to.be.an('object'); + + // expect(data).to.have.all.keys('bidId', 'imp', 'site', 'deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'coppa'); + + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.coppa).to.be.a('number'); + expect(data.language).to.be.a('string'); + // expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + + expect(data.city).to.be.a('string'); + expect(data.geo).to.be.a('object'); + // expect(data.userId).to.be.a('string'); + // expect(data.imp).to.be.a('object'); + }); + // it('Returns empty data if no valid requests are passed', function () { + /// serverRequest = spec.buildRequests([]); + // let data = serverRequest.data; + // expect(data.imp).to.be.an('array').that.is.empty; + // }); + }); + + describe('with COPPA', function () { + beforeEach(function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + }); + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send the Coppa "required" flag set to "1" in the request', function () { + let serverRequest = spec.buildRequests([bid]); + expect(serverRequest.data.coppa).to.equal(1); + }); + }); + + describe('interpretResponse', function () { + it('should get correct bid response', function () { + let response = { + + bidderRequestId: 'f6adb85f-4e19-45a0-b41e-2a5b9a48f23a', + + seatbid: [ + { + bid: [ + { + id: '123', + impid: 'b4f290d7-d4ab-4778-ab94-2baf06420b22', + price: DEFAULT_PRICE, + adm: 'hi', + cid: 'test_cid', + crid: 'test_banner_crid', + w: DEFAULT_BANNER_WIDTH, + h: DEFAULT_BANNER_HEIGHT, + adomain: [], + } + ], + seat: BIDDER_CODE + } + ], + } + + let expectedResponse = [ + { + requestId: 'b4f290d7-d4ab-4778-ab94-2baf06420b22', + cpm: DEFAULT_PRICE, + width: DEFAULT_BANNER_WIDTH, + height: DEFAULT_BANNER_HEIGHT, + creativeId: 'test_banner_crid', + ad: 'hi', + currency: DEFAULT_CURRENCY, + netRevenue: true, + ttl: 300, + meta: { advertiserDomains: [] }, + } + ] + let result = spec.interpretResponse({ body: response }) + + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])) + }) + }) + describe('getUserSyncs', function () { + const syncUrl = 'https://ck.2trk.info/rtb/user/usersync.aspx?id=NA&gdpr=0&gdpr_consent=&us_privacy=&t=4'; + const syncOptions = { + iframeEnabled: true + }; + let userSync = spec.getUserSyncs(syncOptions); + it('Returns valid URL and type', function () { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync).to.deep.equal([ + { type: 'iframe', url: syncUrl } + ]); + }); + }); +}); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index e9ce4c8ccd3..673a821b497 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -12,7 +12,7 @@ import { isFloorsDataValid, addBidResponseHook, fieldMatchingFunctions, - allowedFields + allowedFields, parseFloorData, normalizeDefault, getFloorDataFromAdUnits } from 'modules/priceFloors.js'; import * as events from 'src/events.js'; import * as mockGpt from '../integration/faker/googletag.js'; @@ -20,6 +20,9 @@ import 'src/prebid.js'; import {createBid} from '../../../src/bidfactory.js'; import {auctionManager} from '../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; +import {guardTids} from '../../../src/adapters/bidderFactory.js'; +import * as activities from '../../../src/activities/rules.js'; +import {server} from '../../mocks/xhr.js'; describe('the price floors module', function () { let logErrorSpy; @@ -120,7 +123,7 @@ describe('the price floors module', function () { return { code, mediaTypes: {banner: { sizes: [[300, 200], [300, 600]] }, native: {}}, - bids: [{bidder: 'someBidder'}, {bidder: 'someOtherBidder'}] + bids: [{bidder: 'someBidder', adUnitCode: code}, {bidder: 'someOtherBidder', adUnitCode: code}] }; } beforeEach(function() { @@ -140,6 +143,76 @@ describe('the price floors module', function () { getGlobal().bidderSettings = {}; }); + describe('parseFloorData', () => { + it('should accept just a default floor', () => { + const fd = parseFloorData({ + default: 1.23 + }); + expect(getFirstMatchingFloor(fd, {}, {}).matchingFloor).to.eql(1.23); + }); + }); + + describe('getFloorDataFromAdUnits', () => { + let adUnits; + + function setFloorValues(rule) { + adUnits.forEach((au, i) => { + au.floors = { + values: { + [rule]: i + 1 + } + } + }) + } + + beforeEach(() => { + adUnits = ['au1', 'au2', 'au3'].map(getAdUnitMock); + }) + + it('should use one schema for all adUnits', () => { + setFloorValues('*;*') + adUnits[1].floors.schema = { + fields: ['mediaType', 'gptSlot'], + delimiter: ';' + } + sinon.assert.match(getFloorDataFromAdUnits(adUnits), { + schema: { + fields: ['adUnitCode', 'mediaType', 'gptSlot'], + delimiter: ';' + }, + values: { + 'au1;*;*': 1, + 'au2;*;*': 2, + 'au3;*;*': 3 + } + }) + }); + it('should ignore adUnits that declare different schema', () => { + setFloorValues('*|*'); + adUnits[0].floors.schema = { + fields: ['mediaType', 'gptSlot'] + }; + adUnits[2].floors.schema = { + fields: ['gptSlot', 'mediaType'] + }; + expect(getFloorDataFromAdUnits(adUnits).values).to.eql({ + 'au1|*|*': 1, + 'au2|*|*': 2 + }) + }); + it('should ignore adUnits that declare no values', () => { + setFloorValues('*'); + adUnits[0].floors.schema = { + fields: ['mediaType'] + }; + delete adUnits[2].floors.values; + expect(getFloorDataFromAdUnits(adUnits).values).to.eql({ + 'au1|*': 1, + 'au2|*': 2, + }) + }) + }) + describe('getFloorsDataForAuction', function () { it('converts basic input floor data into a floorData map for the auction correctly', function () { // basic input where nothing needs to be updated @@ -230,8 +303,8 @@ describe('the price floors module', function () { }); describe('getFirstMatchingFloor', function () { - it('uses a 0 floor as overrite', function () { - let inputFloorData = { + it('uses a 0 floor as override', function () { + let inputFloorData = normalizeDefault({ currency: 'USD', schema: { delimiter: '|', @@ -242,7 +315,7 @@ describe('the price floors module', function () { 'test_div_2': 2 }, default: 0.5 - }; + }); expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ floorMin: 0, @@ -431,7 +504,7 @@ describe('the price floors module', function () { }); }); it('selects the right floor for more complex rules', function () { - let inputFloorData = { + let inputFloorData = normalizeDefault({ currency: 'USD', schema: { delimiter: '^', @@ -445,7 +518,7 @@ describe('the price floors module', function () { 'weird_div^*^300x250': 5.5 }, default: 0.5 - }; + }); // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ floorMin: 0, @@ -487,10 +560,8 @@ describe('the price floors module', function () { matchingFloor: undefined }); // if default is there use it - inputFloorData = { default: 5.0 }; - expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ - matchingFloor: 5.0 - }); + inputFloorData = normalizeDefault({ default: 5.0 }); + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'}).matchingFloor).to.equal(5.0); }); describe('with gpt enabled', function () { let gptFloorData; @@ -591,16 +662,11 @@ describe('the price floors module', function () { adUnits, }); }; - let fakeFloorProvider; let actualAllowedFields = allowedFields; let actualFieldMatchingFunctions = fieldMatchingFunctions; const defaultAllowedFields = [...allowedFields]; const defaultMatchingFunctions = {...fieldMatchingFunctions}; - beforeEach(function() { - fakeFloorProvider = sinon.fakeServer.create(); - }); afterEach(function() { - fakeFloorProvider.restore(); exposedAdUnits = undefined; actualAllowedFields = [...defaultAllowedFields]; actualFieldMatchingFunctions = {...defaultMatchingFunctions}; @@ -695,6 +761,95 @@ describe('the price floors module', function () { floorProvider: undefined }); }); + describe('default floor', () => { + let adUnits; + beforeEach(() => { + adUnits = ['au1', 'au2'].map(getAdUnitMock); + }) + function expectFloors(floors) { + runStandardAuction(adUnits); + adUnits.forEach((au, i) => { + au.bids.forEach(bid => { + expect(bid.getFloor().floor).to.eql(floors[i]); + }) + }) + } + describe('should be sufficient by itself', () => { + it('globally', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + default: 1.23 + } + }); + expectFloors([1.23, 1.23]) + }); + it('on adUnits', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = {default: 1}; + adUnits[1].floors = {default: 2}; + expectFloors([1, 2]) + }); + it('on an adUnit with hidden schema', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + default: 1 + } + adUnits[1].floors = { + default: 2 + } + expectFloors([1, 2]); + }) + }); + describe('should NOT be used when a star rule exists', () => { + it('globally', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + values: { + '*|*': 2 + }, + default: 3, + } + }); + expectFloors([2, 2]); + }); + it('on adUnits', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + values: { + '*|*': 1 + }, + default: 3 + }; + adUnits[1].floors = { + values: { + '*|*': 2 + }, + default: 4 + } + expectFloors([1, 2]); + }) + }); + }) it('bidRequests should have getFloor function and flooring meta data when setConfig occurs', function () { handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider'}); runStandardAuction(); @@ -984,7 +1139,7 @@ describe('the price floors module', function () { }); }); it('Should continue auction of delay is hit without a response from floor provider', function () { - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json//'}}); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1011,7 +1166,7 @@ describe('the price floors module', function () { fetchStatus: 'timeout', floorProvider: undefined }); - fakeFloorProvider.respond(); + server.respond(); }); it('It should fetch if config has url and bidRequests have fetch level flooring meta data', function () { // init the fake server with response stuff @@ -1019,14 +1174,14 @@ describe('the price floors module', function () { ...basicFloorData, modelVersion: 'fetch model name', // change the model name }; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', 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'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1035,7 +1190,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); expect(exposedAdUnits).to.not.be.undefined; // the exposedAdUnits should be from the fetch not setConfig level data @@ -1059,14 +1214,14 @@ describe('the price floors module', function () { floorProvider: 'floorProviderD', // change the floor provider modelVersion: 'fetch model name', // change the model name }; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorproviderC', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorproviderC', 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'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1075,7 +1230,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); // the exposedAdUnits should be from the fetch not setConfig level data // and fetchStatus is success since fetch worked @@ -1100,14 +1255,14 @@ describe('the price floors module', function () { modelVersion: 'fetch model name', // change the model name }; fetchFloorData.skipRate = 95; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', 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'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1116,7 +1271,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); expect(exposedAdUnits).to.not.be.undefined; // the exposedAdUnits should be from the fetch not setConfig level data @@ -1135,10 +1290,10 @@ describe('the price floors module', function () { }); it('Should not break if floor provider returns 404', function () { // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // run the auction and make server respond with 404 - fakeFloorProvider.respond(); + server.respond(); runStandardAuction(); // error should have been called for fetch error @@ -1158,13 +1313,13 @@ describe('the price floors module', function () { }); }); it('Should not break if floor provider returns non json', function () { - fakeFloorProvider.respondWith('Not valid response'); + server.respondWith('Not valid response'); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // run the auction and make server respond - fakeFloorProvider.respond(); + server.respond(); runStandardAuction(); // error should have been called for response floor data not being valid @@ -1185,27 +1340,27 @@ describe('the price floors module', function () { }); it('should handle not using fetch correctly', function () { // run setConfig twice indicating fetch - fakeFloorProvider.respondWith(JSON.stringify(basicFloorData)); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + server.respondWith(JSON.stringify(basicFloorData)); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // log warn should be called and server only should have one request expect(logWarnSpy.calledOnce).to.equal(true); - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // now we respond and then run again it should work and make another request - fakeFloorProvider.respond(); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); - fakeFloorProvider.respond(); + server.respond(); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); + server.respond(); // now warn still only called once and server called twice expect(logWarnSpy.calledOnce).to.equal(true); - expect(fakeFloorProvider.requests.length).to.equal(2); + expect(server.requests.length).to.equal(2); // should log error if method is not GET for now expect(logErrorSpy.calledOnce).to.equal(false); - handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakeFloorProvider.json', method: 'POST'}}); + handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakefloorprovider.json/', method: 'POST'}}); expect(logErrorSpy.calledOnce).to.equal(true); }); describe('isFloorsDataValid', function () { @@ -1369,10 +1524,22 @@ describe('the price floors module', function () { floor: 2.5 }); }); + + it('works when TIDs are disabled', () => { + sandbox.stub(activities, 'isActivityAllowed').returns(false); + const req = utils.deepClone(bidRequest); + _floorDataForAuction[req.auctionId] = utils.deepClone(basicFloorConfig); + + expect(guardTids('mock-bidder').bidRequest(req).getFloor({})).to.deep.equal({ + currency: 'USD', + floor: 1.0 + }); + }); + it('picks the right rule with more complex rules', function () { _floorDataForAuction[bidRequest.auctionId] = { ...basicFloorConfig, - data: { + data: normalizeDefault({ currency: 'USD', schema: { fields: ['mediaType', 'size'], delimiter: '|' }, values: { @@ -1384,7 +1551,7 @@ describe('the price floors module', function () { 'video|*': 5.5 }, default: 10.0 - } + }) }; // assumes banner * @@ -1844,6 +2011,12 @@ describe('the price floors module', function () { expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); expect(logWarnSpy.calledOnce).to.equal(true); }); + it('if it finds a rule with a floor price of zero it should not call log warn', function () { + _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[AUCTION_ID].data.values = { '*': 0 }; + runBidResponse(); + expect(logWarnSpy.calledOnce).to.equal(false); + }); it('if it finds a rule and floors should update the bid accordingly', function () { _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); _floorDataForAuction[AUCTION_ID].data.values = { 'banner': 1.0 }; diff --git a/test/spec/modules/programmaticaBidAdapter_spec.js b/test/spec/modules/programmaticaBidAdapter_spec.js new file mode 100644 index 00000000000..247d20752c3 --- /dev/null +++ b/test/spec/modules/programmaticaBidAdapter_spec.js @@ -0,0 +1,263 @@ +import { expect } from 'chai'; +import { spec } from 'modules/programmaticaBidAdapter.js'; +import { deepClone } from 'src/utils.js'; + +describe('programmaticaBidAdapterTests', function () { + let bidRequestData = { + bids: [ + { + bidId: 'testbid', + bidder: 'programmatica', + params: { + siteId: 'testsite', + placementId: 'testplacement', + }, + sizes: [[300, 250]] + } + ] + }; + let request = []; + + it('validate_pub_params', function () { + expect( + spec.isBidRequestValid({ + bidder: 'programmatica', + params: { + siteId: 'testsite', + placementId: 'testplacement', + } + }) + ).to.equal(true); + }); + + it('validate_generated_url', function () { + const request = spec.buildRequests(deepClone(bidRequestData.bids), { timeout: 1234 }); + let req_url = request[0].url; + + expect(req_url).to.equal('https://asr.programmatica.com/get'); + }); + + it('validate_response_params', function () { + let serverResponse = { + body: { + 'id': 'crid', + 'type': { + 'format': 'Image', + 'source': 'passback', + 'dspId': '', + 'dspCreativeId': '' + }, + 'content': { + 'data': 'test ad', + 'imps': null, + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '300x250', + 'matching': '', + 'cpm': 10, + 'currency': 'USD' + } + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(10); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crid'); + expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']); + }); + + it('validate_response_params_imps', function () { + let serverResponse = { + body: { + 'id': 'crid', + 'type': { + 'format': 'Image', + 'source': 'passback', + 'dspId': '', + 'dspCreativeId': '' + }, + 'content': { + 'data': 'test ad', + 'imps': [ + 'testImp' + ], + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '300x250', + 'matching': '', + 'cpm': 10, + 'currency': 'USD' + } + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(10); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crid'); + expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']); + }) + + it('validate_invalid_response', function () { + let serverResponse = { + body: {} + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(0); + }) + + it('video_bid', function () { + const bidRequest = deepClone(bidRequestData.bids); + bidRequest[0].mediaTypes = { + video: { + playerSize: [234, 765] + } + }; + + const request = spec.buildRequests(bidRequest, { timeout: 1234 }); + const vastXml = ''; + let serverResponse = { + body: { + 'id': 'cki2n3n6snkuulqutpf0', + 'type': { + 'format': '', + 'source': 'rtb', + 'dspId': '1' + }, + 'content': { + 'data': vastXml, + 'imps': [ + 'https://asr.dev.programmatica.com/track/imp' + ], + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '', + 'matching': '', + 'cpm': 70, + 'currency': 'RUB' + } + }; + + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.mediaType).to.equal('video'); + expect(bid.vastXml).to.equal(vastXml); + expect(bid.width).to.equal(234); + expect(bid.height).to.equal(765); + }); +}); + +describe('getUserSyncs', function() { + it('returns empty sync array', function() { + const syncOptions = {}; + + expect(spec.getUserSyncs(syncOptions)).to.deep.equal([]); + }); + + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({ + pixelEnabled: true, + }, {}, {}, '1---'); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp?usp=1---&consent=') + }); + + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: true + }, + }, + } + }, ''); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=1') + }); + + it('Should return array of objects with proper sync config , include GDPR, no purpose', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: false + }, + }, + } + }, ''); + expect(syncData).is.empty; + }); + + it('Should return array of objects with proper sync config , GDPR not applies', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: false, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + }, ''); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=0') + }); +}) diff --git a/test/spec/modules/pubCircleBidAdapter_spec.js b/test/spec/modules/pubCircleBidAdapter_spec.js new file mode 100644 index 00000000000..8aaa023ee1c --- /dev/null +++ b/test/spec/modules/pubCircleBidAdapter_spec.js @@ -0,0 +1,399 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/pubCircleBidAdapter'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'pubcircle' + +describe('PubCircleBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://ml.pubcircle.ai/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.pubcircle.ai/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.pubcircle.ai/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js index 7d98b724bd8..5ad58ea1a37 100644 --- a/test/spec/modules/publinkIdSystem_spec.js +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -72,11 +72,6 @@ describe('PublinkIdSystem', () => { expect(result.callback).to.be.a('function'); }); - it('Use local copy', () => { - const result = publinkIdSubmodule.getId({}, undefined, TEST_COOKIE_VALUE); - expect(result).to.be.undefined; - }); - describe('callout for id', () => { let callbackSpy = sinon.spy(); @@ -84,6 +79,44 @@ describe('PublinkIdSystem', () => { callbackSpy.resetHistory(); }); + it('Has cached id', () => { + const config = {storage: {type: 'cookie'}}; + let submoduleCallback = publinkIdSubmodule.getId(config, undefined, TEST_COOKIE_VALUE).callback; + submoduleCallback(callbackSpy); + + const request = server.requests[0]; + const parsed = parseUrl(request.url); + + expect(parsed.hostname).to.equal('proc.ad.cpe.dotomi.com'); + expect(parsed.pathname).to.equal('/cvx/client/sync/publink/refresh'); + expect(parsed.search.mpn).to.equal('Prebid.js'); + expect(parsed.search.mpv).to.equal('$prebid.version$'); + expect(parsed.search.publink).to.equal(TEST_COOKIE_VALUE); + + request.respond(200, {}, JSON.stringify(serverResponse)); + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink); + }); + + it('Request path has priority', () => { + const config = {storage: {type: 'cookie'}, params: {e: 'ca11c0ca7', site_id: '102030'}}; + let submoduleCallback = publinkIdSubmodule.getId(config, undefined, TEST_COOKIE_VALUE).callback; + submoduleCallback(callbackSpy); + + const request = server.requests[0]; + const parsed = parseUrl(request.url); + + expect(parsed.hostname).to.equal('proc.ad.cpe.dotomi.com'); + expect(parsed.pathname).to.equal('/cvx/client/sync/publink'); + expect(parsed.search.mpn).to.equal('Prebid.js'); + expect(parsed.search.mpv).to.equal('$prebid.version$'); + expect(parsed.search.publink).to.equal(TEST_COOKIE_VALUE); + + request.respond(200, {}, JSON.stringify(serverResponse)); + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink); + }); + it('Fetch with consent data', () => { const config = {storage: {type: 'cookie'}, params: {e: 'ca11c0ca7', site_id: '102030'}}; const consentData = {gdprApplies: 1, consentString: 'myconsentstring'}; @@ -120,7 +153,7 @@ describe('PublinkIdSystem', () => { expect(parsed.search.mpn).to.equal('Prebid.js'); expect(parsed.search.mpv).to.equal('$prebid.version$'); - request.respond(204, {}, JSON.stringify(serverResponse)); + request.respond(204); expect(callbackSpy.called).to.be.false; }); diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index c56ed565c43..7fce08fc24f 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -1,11 +1,11 @@ -import pubmaticAnalyticsAdapter, { getMetadata } from 'modules/pubmaticAnalyticsAdapter.js'; +import pubmaticAnalyticsAdapter, {getMetadata} from 'modules/pubmaticAnalyticsAdapter.js'; import adapterManager from 'src/adapterManager.js'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config.js'; -import { - setConfig, - addBidResponseHook, -} from 'modules/currency.js'; +import { setConfig } from 'modules/currency.js'; +import { server } from '../../mocks/xhr.js'; +import 'src/prebid.js'; +import { getGlobal } from 'src/prebidGlobal'; let events = require('src/events'); let ajax = require('src/ajax'); @@ -23,6 +23,7 @@ const { AUCTION_END, BID_REQUESTED, BID_RESPONSE, + BID_REJECTED, BIDDER_DONE, BID_WON, BID_TIMEOUT, @@ -108,6 +109,9 @@ const BID2 = Object.assign({}, BID, { } }); +const BID3 = Object.assign({}, BID2, { + rejectionReason: CONSTANTS.REJECTION_REASON.FLOOR_NOT_MET +}) const MOCK = { SET_TARGETING: { [BID.adUnitCode]: BID.adserverTargeting, @@ -237,6 +241,9 @@ const MOCK = { BID, BID2 ], + REJECTED_BID: [ + BID3 + ], AUCTION_END: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' }, @@ -273,7 +280,6 @@ function getLoggerJsonFromRequest(requestBody) { describe('pubmatic analytics adapter', function () { let sandbox; - let xhr; let requests; let oldScreen; let clock; @@ -282,9 +288,7 @@ describe('pubmatic analytics adapter', function () { setUADefault(); sandbox = sinon.sandbox.create(); - xhr = sandbox.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + requests = server.requests; sandbox.stub(events, 'getEvents').returns([]); @@ -364,6 +368,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 @@ -383,9 +388,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(1); expect(data.s[0].ps[0].t).to.equal(0); @@ -393,7 +399,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); - expect(data.s[0].ps[0].frv).to.equal(undefined); + expect(data.s[0].ps[0].frv).to.equal(1.1); // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); @@ -414,7 +420,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -422,7 +429,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // tracker slot1 let firstTracker = requests[0].url; @@ -452,6 +459,94 @@ describe('pubmatic analytics adapter', function () { expect(data.af).to.equal('video'); }); + it('Logger: do not log floor fields when prebids floor shows noData in location property', function() { + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY['bids'][1]['floorData']['location'] = 'noData'; + + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + + let data = getLoggerJsonFromRequest(request.requestBody); + + expect(data.pubid).to.equal('9999'); + expect(data.fmv).to.equal(undefined); + + // slot 1 + expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + + // slot 2 + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].au).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(undefined); + }); + + it('Logger: log floor fields when prebids floor shows setConfig in location property', function() { + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY['bids'][1]['floorData']['location'] = 'setConfig'; + + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + + let data = getLoggerJsonFromRequest(request.requestBody); + + expect(data.pubid).to.equal('9999'); + expect(data.fmv).to.equal('floorModelTest'); + + // slot 1 + expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + + // slot 2 + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].au).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(0); + }); + it('bidCpmAdjustment: USD: Logger: best case + win tracker', function() { const bidCopy = utils.deepClone(BID); bidCopy.cpm = bidCopy.originalCpm * 2; // bidCpmAdjustment => bidCpm * 2 @@ -481,6 +576,7 @@ describe('pubmatic analytics adapter', function () { expect(data.pid).to.equal('1111'); expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); expect(data.tgid).to.equal(0); @@ -501,7 +597,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // tracker slot1 let firstTracker = requests[0].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -555,6 +651,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(0);// test group id should be between 0-15 else set to 0 expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 @@ -617,7 +714,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].psz).to.equal('0x0'); expect(data.s[1].ps[0].eg).to.equal(0); expect(data.s[1].ps[0].en).to.equal(0); - expect(data.s[1].ps[0].di).to.equal(''); + expect(data.s[1].ps[0].di).to.equal('-1'); expect(data.s[1].ps[0].dc).to.equal(''); expect(data.s[1].ps[0].mi).to.equal(undefined); expect(data.s[1].ps[0].l1).to.equal(0); @@ -655,7 +752,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].psz).to.equal('0x0'); expect(data.s[1].ps[0].eg).to.equal(0); expect(data.s[1].ps[0].en).to.equal(0); - expect(data.s[1].ps[0].di).to.equal(''); + expect(data.s[1].ps[0].di).to.equal('-1'); expect(data.s[1].ps[0].dc).to.equal(''); expect(data.s[1].ps[0].mi).to.equal(undefined); expect(data.s[1].ps[0].l1).to.equal(0); @@ -703,7 +800,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(0); + expect(data.s[0].ps[0].ol1).to.equal(0); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(1); @@ -711,7 +809,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); }); it('Logger: currency conversion check', function() { @@ -762,7 +860,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -810,7 +909,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -818,7 +918,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); expect(data.dvc).to.deep.equal({'plt': 2}); // respective tracker slot let firstTracker = requests[1].url; @@ -867,7 +967,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -876,7 +977,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); expect(data.dvc).to.deep.equal({'plt': 1}); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // respective tracker slot let firstTracker = requests[1].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -921,7 +1022,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -929,7 +1031,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // respective tracker slot let firstTracker = requests[1].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -976,7 +1078,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -992,6 +1095,61 @@ describe('pubmatic analytics adapter', function () { expect(data.kgpv).to.equal('*'); }); + it('Logger: to handle floor rejected bids', function() { + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_REJECTED, MOCK.REJECTED_BID[0]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(2); // 1 logger and 1 win-tracker + let request = requests[1]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + + // slot 2 + // Testing only for rejected bid as other scenarios will be covered under other TCs + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[0].ps[0].bc).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].piid).to.equal('partnerImpressionID-2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(0); // Net CPM is market as 0 due to bid rejection + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].adv).to.equal('example.com'); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(1); + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.s[1].ps[0].frv).to.equal(1.1); + }); + it('Logger: best case + win tracker in case of Bidder Aliases', function() { MOCK.BID_REQUESTED['bids'][0]['bidder'] = 'pubmatic_alias'; MOCK.BID_REQUESTED['bids'][0]['bidderCode'] = 'pubmatic_alias'; @@ -1030,6 +1188,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); @@ -1039,7 +1198,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic_alias'); @@ -1051,9 +1210,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(0); expect(data.s[0].ps[0].t).to.equal(0); @@ -1083,7 +1243,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -1091,7 +1252,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // tracker slot1 let firstTracker = requests[0].url; @@ -1149,6 +1310,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); @@ -1158,7 +1320,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('groupm'); @@ -1170,9 +1332,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(0); expect(data.s[0].ps[0].t).to.equal(0); @@ -1201,7 +1364,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 5d8d762e793..154a8e1253b 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -82,6 +82,7 @@ describe('PubMatic adapter', function () { ortb2Imp: { ext: { tid: '92489f71-1bf2-49a0-adf9-000cea934729', + gpid: '/1111/homepage-leftnav' } }, schain: schainConfig @@ -1133,7 +1134,19 @@ describe('PubMatic adapter', function () { ortb2: { source: { tid: 'source-tid' - } + }, + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, } }); let data = JSON.parse(request.data); @@ -1144,10 +1157,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.source.tid).to.equal('source-tid'); // Prebid TransactionId @@ -1160,6 +1173,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid expect(data.imp[0].ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); @@ -1387,7 +1401,21 @@ describe('PubMatic adapter', function () { it('Request params check: without adSlot', function () { delete bidRequests[0].params.adSlot; let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' + auctionId: 'new-auction-id', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }); let data = JSON.parse(request.data); expect(data.at).to.equal(1); // auction type @@ -1397,10 +1425,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -1413,6 +1441,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].banner.w).to.equal(728); // width expect(data.imp[0].banner.h).to.equal(90); // height expect(data.imp[0].banner.format).to.deep.equal([{w: 160, h: 600}]); + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); @@ -1596,7 +1625,21 @@ describe('PubMatic adapter', function () { it('Pass auctiondId as wiid if wiid is not passed in params', function () { let bidRequest = { - auctionId: 'new-auction-id' + auctionId: 'new-auction-id', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }; delete bidRequests[0].params.wiid; let request = spec.buildRequests(bidRequests, bidRequest); @@ -1608,10 +1651,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Longitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Longitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal('new-auction-id'); // OpenWrap: Wrapper Impression ID @@ -1623,6 +1666,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid }); @@ -1631,6 +1675,20 @@ describe('PubMatic adapter', function () { gdprConsent: { consentString: 'kjfdniwjnifwenrif3', gdprApplies: true + }, + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, } }; let request = spec.buildRequests(bidRequests, bidRequest); @@ -1644,10 +1702,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -1657,6 +1715,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid @@ -1664,7 +1723,21 @@ describe('PubMatic adapter', function () { it('Request params check with USP/CCPA Consent', function () { let bidRequest = { - uspConsent: '1NYN' + uspConsent: '1NYN', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }; let request = spec.buildRequests(bidRequests, bidRequest); let data = JSON.parse(request.data); @@ -1676,10 +1749,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -1691,6 +1764,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid // second request without USP/CCPA @@ -1841,6 +1915,33 @@ describe('PubMatic adapter', function () { }); describe('ortb2Imp', function() { + describe('ortb2Imp.ext.gpid', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should send gpid if imp[].ext.gpid is specified', function() { + bidRequests[0].ortb2Imp = { + ext: { + gpid: 'ortb2Imp.ext.gpid' + } + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext).to.have.property('gpid'); + expect(data.imp[0].ext.gpid).to.equal('ortb2Imp.ext.gpid'); + }); + + it('should not send if imp[].ext.gpid is not specified', function() { + bidRequests[0].ortb2Imp = { ext: { } }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext).to.not.have.property('gpid'); + }); + }); + describe('ortb2Imp.ext.data.pbadslot', function() { beforeEach(function () { if (bidRequests[0].hasOwnProperty('ortb2Imp')) { @@ -2160,7 +2261,7 @@ describe('PubMatic adapter', function () { sandbox.restore(); }); - describe('AdsrvrOrgId from userId module', function() { + describe('userIdAsEids', function() { let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -2170,13 +2271,10 @@ describe('PubMatic adapter', function () { sandbox.restore(); }); - it('Request should have AdsrvrOrgId config params', function() { + it('Request should have EIDs', function() { bidRequests[0].userId = {}; bidRequests[0].userId.tdid = 'TTD_ID_FROM_USER_ID_MODULE'; - 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([{ + bidRequests[0].userIdAsEids = [{ 'source': 'adserver.org', 'uids': [{ 'id': 'TTD_ID_FROM_USER_ID_MODULE', @@ -2185,383 +2283,19 @@ describe('PubMatic adapter', function () { 'rtiPartner': 'TDID' } }] - }]); - }); - - it('Request should have adsrvrOrgId from UserId Module if config and userId module both have TTD ID', function() { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - adsrvrOrgId: { - 'TDID': 'TTD_ID_FROM_CONFIG', - 'TDID_LOOKUP': 'TRUE', - 'TDID_CREATED_AT': '2018-10-01T07:05:40' - } - }; - return config[key]; - }); - bidRequests[0].userId = {}; - bidRequests[0].userId.tdid = 'TTD_ID_FROM_USER_ID_MODULE'; - 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': 'adserver.org', - 'uids': [{ - 'id': 'TTD_ID_FROM_USER_ID_MODULE', - 'atype': 1, - 'ext': { - 'rtiPartner': 'TDID' - } - }] - }]); - }); - - it('Request should NOT have adsrvrOrgId params if userId is NOT object', function() { + }]; let request = spec.buildRequests(bidRequests, {}); let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal(undefined); + expect(data.user.eids).to.deep.equal(bidRequests[0].userIdAsEids); }); - it('Request should NOT have adsrvrOrgId params if userId.tdid is NOT string', function() { - bidRequests[0].userId = { - tdid: 1234 - }; + it('Request should NOT have EIDs userIdAsEids is NOT object', function() { let request = spec.buildRequests(bidRequests, {}); let data = JSON.parse(request.data); expect(data.user.eids).to.deep.equal(undefined); }); }); - describe('UserIds from request', function() { - describe('pubcommon Id', function() { - it('send the pubcommon id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.pubcid = 'pub_common_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': 'pubcid.org', - 'uids': [{ - 'id': 'pub_common_user_id', - 'atype': 1 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.pubcid = 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.pubcid = []; - 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.pubcid = 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.pubcid = {}; - 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 = {}; - bidRequests[0].userId.id5id = { uid: 'id5-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': 'id5-sync.com', - 'uids': [{ - 'id': 'id5-user-id', - 'atype': 1 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.id5id = { uid: 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.id5id = { uid: [] }; - 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.id5id = { uid: 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.id5id = { uid: {} }; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('Criteo Id', function() { - it('send the criteo id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.criteoId = 'criteo-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': 'criteo.com', - 'uids': [{ - 'id': 'criteo-user-id', - 'atype': 1 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.criteoId = 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.criteoId = []; - 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.criteoId = 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.criteoId = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('IdentityLink Id', function() { - it('send the identity-link id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.idl_env = 'identity-link-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': 'liveramp.com', - 'uids': [{ - 'id': 'identity-link-user-id', - 'atype': 3 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.idl_env = 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.idl_env = []; - 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.idl_env = 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.idl_env = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('LiveIntent Id', function() { - it('send the LiveIntent id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.lipb = { lipbid: 'live-intent-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': 'liveintent.com', - 'uids': [{ - 'id': 'live-intent-user-id', - 'atype': 3 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.lipb = { lipbid: 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.lipb.lipbid = []; - 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.lipb.lipbid = 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.lipb.lipbid = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('Parrable Id', function() { - it('send the Parrable id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.parrableId = { eid: 'parrable-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': 'parrable.com', - 'uids': [{ - 'id': 'parrable-user-id', - 'atype': 1 - }] - }]); - }); - - it('do not pass if not object with eid key', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.parrableid = 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.parrableid = []; - 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.parrableid = 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.parrableid = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('Britepool Id', function() { - it('send the Britepool id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.britepoolid = 'britepool-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': 'britepool.com', - 'uids': [{ - 'id': 'britepool-user-id', - 'atype': 3 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.britepoolid = 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.britepoolid = []; - 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.britepoolid = 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.britepoolid = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('NetId', function() { - it('send the NetId if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.netId = 'netid-user-id'; - 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': 'netid.de', - 'uids': [{ - 'id': 'netid-user-id', - 'atype': 1 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.netId = 1; - 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.netId = []; - 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.netId = 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.netId = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - }); - it('should pass device.sua if present in bidderRequest fpd ortb2 object', function () { const suaObject = {'source': 2, 'platform': {'brand': 'macOS', 'version': ['12', '4', '0']}, 'browsers': [{'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']}], 'mobile': 0, 'model': '', 'bitness': '64', 'architecture': 'x86'}; let request = spec.buildRequests(multipleMediaRequests, { @@ -2577,6 +2311,23 @@ describe('PubMatic adapter', function () { expect(data.device.sua).to.deep.equal(suaObject); }); + it('should pass device.ext.cdep if present in bidderRequest fpd ortb2 object', function () { + const cdepObj = { + cdep: 'example_label_1' + }; + let request = spec.buildRequests(multipleMediaRequests, { + auctionId: 'new-auction-id', + ortb2: { + device: { + ext: cdepObj + } + } + }); + let data = JSON.parse(request.data); + expect(data.device.ext.cdep).to.exist.and.to.be.an('string'); + expect(data.device.ext).to.deep.equal(cdepObj); + }); + it('Request params should have valid native bid request for all valid params', function () { let request = spec.buildRequests(nativeBidRequests, { auctionId: 'new-auction-id' @@ -2692,7 +2443,21 @@ describe('PubMatic adapter', function () { it('Request params check for 1 banner and 1 video ad', function () { let request = spec.buildRequests(multipleMediaRequests, { - auctionId: 'new-auction-id' + auctionId: 'new-auction-id', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }); let data = JSON.parse(request.data); @@ -2706,10 +2471,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(multipleMediaRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(multipleMediaRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(multipleMediaRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(multipleMediaRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(multipleMediaRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(multipleMediaRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(multipleMediaRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(multipleMediaRequests[0].transactionId); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(multipleMediaRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -3057,6 +2822,76 @@ describe('PubMatic adapter', function () { }); } + describe('GPP', function() { + it('Request params check with GPP Consent', function () { + let bidRequest = { + gppConsent: { + 'gppString': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'fullGppData': { + 'sectionId': 3, + 'gppVersion': 1, + 'sectionList': [ + 5, + 7 + ], + 'applicableSections': [ + 5 + ], + 'gppString': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'pingData': { + 'cmpStatus': 'loaded', + 'gppVersion': '1.0', + 'cmpDisplayStatus': 'visible', + 'supportedAPIs': [ + 'tcfca', + 'usnat', + 'usca', + 'usva', + 'usco', + 'usut', + 'usct' + ], + 'cmpId': 31 + }, + 'eventName': 'sectionChange' + }, + 'applicableSections': [ + 5 + ], + 'apiVersion': 1 + } + }; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); + expect(data.regs.gpp_sid[0]).to.equal(5); + }); + + it('Request params check without GPP Consent', function () { + let bidRequest = {}; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.regs).to.equal(undefined); + }); + + it('Request params check with GPP Consent read from ortb2', function () { + let bidRequest = { + ortb2: { + regs: { + 'gpp': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'gpp_sid': [ + 5 + ] + } + } + }; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); + expect(data.regs.gpp_sid[0]).to.equal(5); + }); + }); + describe('Fledge', function() { it('should not send imp.ext.ae when FLEDGE is disabled, ', function () { let bidRequest = Object.assign([], bidRequests); @@ -3816,6 +3651,89 @@ describe('PubMatic adapter', function () { } }); + describe('Fledge Auction config Response', function () { + let response; + let bidRequestConfigs = [ + { + bidder: 'pubmatic', + mediaTypes: { + banner: { + sizes: [[728, 90], [160, 600]] + } + }, + params: { + publisherId: '5670', + adSlot: '/15671365/DMDemo@300x250:0', + kadfloor: '1.2', + pmzoneid: 'aabc, ddef', + kadpageurl: 'www.publisher.com', + yob: '1986', + gender: 'M', + lat: '12.3', + lon: '23.7', + wiid: '1234567890', + profId: '100', + verId: '200', + currency: 'AUD', + dctr: 'key1:val1,val2|key2:val1' + }, + placementCode: '/19968336/header-bid-tag-1', + sizes: [[300, 250], [300, 600]], + bidId: 'test_bid_id', + requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + ortb2Imp: { + ext: { + tid: '92489f71-1bf2-49a0-adf9-000cea934729', + ae: 1 + } + }, + } + ]; + + let bidRequest = spec.buildRequests(bidRequestConfigs, {}); + let bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test_bid_id', + price: 2, + w: 728, + h: 250, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup' + }] + }], + cur: 'AUS', + ext: { + fledge_auction_configs: { + 'test_bid_id': { + seller: 'ads.pubmatic.com', + interestGroupBuyers: ['dsp1.com'], + sellerTimeout: 0, + perBuyerSignals: { + 'dsp1.com': { + bid_macros: 0.1, + disallowed_adv_ids: [ + '5678', + '5890' + ], + } + } + } + } + } + }; + + response = spec.interpretResponse({ body: bidResponse }, bidRequest); + it('should return FLEDGE auction_configs alongside bids', function () { + expect(response).to.have.property('bids'); + expect(response).to.have.property('fledgeAuctionConfigs'); + expect(response.fledgeAuctionConfigs.length).to.equal(1); + expect(response.fledgeAuctionConfigs[0].bidId).to.equal('test_bid_id'); + }); + }); + describe('Preapare metadata', function () { it('Should copy all fields from ext to meta', function () { const bid = { @@ -3991,6 +3909,55 @@ describe('PubMatic adapter', function () { type: 'image', url: `${syncurl_image}&gdpr=1&gdpr_consent=foo&us_privacy=1NYN&coppa=1` }]); }); + + describe('GPP', function() { + it('should return userSync url without Gpp consent if gppConsent is undefined', () => { + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, undefined); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should return userSync url without Gpp consent if gppConsent.gppString is undefined', () => { + const gppConsent = { applicableSections: ['5'] }; + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should return userSync url without Gpp consent if gppConsent.applicableSections is undefined', () => { + const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN' }; + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should return userSync url without Gpp consent if gppConsent.applicableSections is an empty array', () => { + const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [] }; + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should concatenate gppString and applicableSections values in the returned userSync iframe url', () => { + const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [5] }; + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gpp=${encodeURIComponent(gppConsent.gppString)}&gpp_sid=${encodeURIComponent(gppConsent.applicableSections)}` + }]); + }); + + it('should concatenate gppString and applicableSections values in the returned userSync image url', () => { + const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [5] }; + const result = spec.getUserSyncs({iframeEnabled: false}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&gpp=${encodeURIComponent(gppConsent.gppString)}&gpp_sid=${encodeURIComponent(gppConsent.applicableSections)}` + }]); + }); + }); }); if (FEATURES.VIDEO) { diff --git a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js index e14582edc39..92d5972cc13 100644 --- a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import pubwiseAnalytics from 'modules/pubwiseAnalyticsAdapter.js'; import {expectEvents} from '../../helpers/analytics.js'; +import {server} from '../../mocks/xhr.js'; let events = require('src/events'); let adapterManager = require('src/adapterManager').default; @@ -9,7 +10,6 @@ let constants = require('src/constants.json'); describe('PubWise Prebid Analytics', function () { let requests; let sandbox; - let xhr; let clock; let mock = {}; @@ -38,9 +38,7 @@ describe('PubWise Prebid Analytics', function () { clock = sandbox.useFakeTimers(); sandbox.stub(events, 'getEvents').returns([]); - xhr = sandbox.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + requests = server.requests; }); afterEach(function () { @@ -50,10 +48,6 @@ describe('PubWise Prebid Analytics', function () { }); describe('enableAnalytics', function () { - beforeEach(function () { - requests = []; - }); - it('should catch all events', function () { pubwiseAnalytics.enableAnalytics(mock.DEFAULT_PW_CONFIG); diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index 1dce87e4b8e..e0f4497a8c8 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -1,13 +1,9 @@ -import pubxaiAnalyticsAdapter from 'modules/pubxaiAnalyticsAdapter.js'; -import { getDeviceType, getBrowser, getOS } from 'modules/pubxaiAnalyticsAdapter.js'; -import { - expect -} from 'chai'; +import pubxaiAnalyticsAdapter, {getBrowser, getDeviceType, getOS} from 'modules/pubxaiAnalyticsAdapter.js'; +import {expect} from 'chai'; import adapterManager from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { - server -} from 'test/mocks/xhr.js'; +import {server} from 'test/mocks/xhr.js'; +import {getGptSlotInfoForAdUnitCode} from '../../../libraries/gptUtils/gptUtils.js'; let events = require('src/events'); let constants = require('src/constants.json'); @@ -527,7 +523,7 @@ describe('pubxai analytics adapter', function() { 'bidderCode': 'appnexus', 'bidId': '248f9a4489835e', 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': utils.getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, + 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', 'sizes': '300x250', 'renderStatus': 2, @@ -596,7 +592,7 @@ describe('pubxai analytics adapter', function() { let expectedAfterBidWon = { 'winningBid': { 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': utils.getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, + 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', 'bidderCode': 'appnexus', 'bidId': '248f9a4489835e', diff --git a/test/spec/modules/pulsepointBidAdapter_spec.js b/test/spec/modules/pulsepointBidAdapter_spec.js index 60dca9e6da0..8db7e909771 100644 --- a/test/spec/modules/pulsepointBidAdapter_spec.js +++ b/test/spec/modules/pulsepointBidAdapter_spec.js @@ -1,7 +1,8 @@ /* eslint dot-notation:0, quote-props:0 */ import {expect} from 'chai'; import {spec} from 'modules/pulsepointBidAdapter.js'; -import {deepClone} from 'src/utils.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {deepClone} from '../../../src/utils'; describe('PulsePoint Adapter Tests', function () { const slotConfigs = [{ @@ -31,39 +32,52 @@ describe('PulsePoint Adapter Tests', function () { cf: '728x90' } }]; + const nativeOrtbRequest = { + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 150, + h: 50, + } + }, + { + id: 2, + required: 1, + title: { + len: 80 + } + }, + { + id: 3, + required: 0, + data: { + type: 1 + } + }] + }; const nativeSlotConfig = [{ placementCode: '/DfpAccount1/slot3', bidId: 'bid12345', - nativeParams: { - title: { required: true, len: 200 }, - image: { wmin: 100 }, - sponsoredBy: { } + mediaTypes: { + native: { + sendTargetingKeys: false, + ortb: nativeOrtbRequest + } }, + nativeOrtbRequest, params: { cp: 'p10000', ct: 't10000' } }]; - const appSlotConfig = [{ - placementCode: '/DfpAccount1/slot3', - bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', - app: { - bundle: 'com.pulsepoint.apps', - storeUrl: 'https://pulsepoint.com/apps', - domain: 'pulsepoint.com', - } - } - }]; const videoSlotConfig = [{ placementCode: '/DfpAccount1/slotVideo', bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', + mediaTypes: { video: { + playerSize: [400, 300], w: 400, h: 300, minduration: 5, @@ -73,6 +87,10 @@ describe('PulsePoint Adapter Tests', function () { minbitrate: 200, protocols: [1, 2, 4] } + }, + params: { + cp: 'p10000', + ct: 't10000' } }]; const additionalParamsConfig = [{ @@ -97,68 +115,6 @@ describe('PulsePoint Adapter Tests', function () { } }]; - const ortbParamsSlotConfig = [{ - placementCode: '/DfpAccount1/slot1', - mediaTypes: { - banner: { - sizes: [[1, 1]] - } - }, - bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', - cf: '1x1', - bcat: ['IAB-1', 'IAB-20'], - battr: [1, 2, 3], - bidfloor: 1.5, - badv: ['cocacola.com', 'lays.com'] - } - }, { - placementCode: '/DfpAccount1/slotVideo', - bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', - video: { - w: 400, - h: 300, - minduration: 5, - maxduration: 10, - }, - battr: [2, 3, 4], - bidfloor: 2.5, - } - }]; - - const outstreamSlotConfig = [{ - placementCode: '/DfpAccount1/slot1', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream' - } - }, - bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', - cf: '1x1', - video: { - h: 300, - w: 400, - minduration: 1, - maxduration: 210, - linearity: 1, - } - }, - renderer: { - options: { - text: 'PulsePoint Outstream' - } - } - }]; - const schainParamsSlotConfig = [{ placementCode: '/DfpAccount1/slot1', mediaTypes: { @@ -200,7 +156,7 @@ describe('PulsePoint Adapter Tests', function () { }; it('Verify build request', function () { - const request = spec.buildRequests(slotConfigs, bidderRequest); + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -208,7 +164,6 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.site).to.not.equal(null); expect(ortbRequest.site.publisher).to.not.equal(null); expect(ortbRequest.site.publisher.id).to.equal('p10000'); - expect(ortbRequest.site.ref).to.equal(bidderRequest.refererInfo.ref); expect(ortbRequest.site.page).to.equal('https://publisher.com/home'); expect(ortbRequest.imp).to.have.lengthOf(2); // device object @@ -217,19 +172,15 @@ describe('PulsePoint Adapter Tests', function () { // slot 1 expect(ortbRequest.imp[0].tagid).to.equal('t10000'); expect(ortbRequest.imp[0].banner).to.not.equal(null); - expect(ortbRequest.imp[0].banner.w).to.equal(300); - expect(ortbRequest.imp[0].banner.h).to.equal(250); + expect(ortbRequest.imp[0].banner.format).to.deep.eq([{'w': 728, 'h': 90}, {'w': 160, 'h': 600}]); // slot 2 expect(ortbRequest.imp[1].tagid).to.equal('t20000'); expect(ortbRequest.imp[1].banner).to.not.equal(null); - expect(ortbRequest.imp[1].banner.w).to.equal(728); - expect(ortbRequest.imp[1].banner.h).to.equal(90); - // tmax - expect(ortbRequest.tmax).to.equal(500); + expect(ortbRequest.imp[1].banner.format).to.deep.eq([{'w': 728, 'h': 90}]); }); it('Verify parse response', function () { - const request = spec.buildRequests(slotConfigs, bidderRequest); + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; const ortbResponse = { seatbid: [{ @@ -237,11 +188,16 @@ describe('PulsePoint Adapter Tests', function () { impid: ortbRequest.imp[0].id, price: 1.25, adm: 'This is an Ad', - crid: 'Creative#123' + crid: 'Creative#123', + mtype: 1, + w: 300, + h: 250, + exp: 20, + adomain: ['advertiser.com'] }] }] }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); + const bids = spec.interpretResponse({body: ortbResponse}, request); expect(bids).to.have.lengthOf(1); // verify first bid const bid = bids[0]; @@ -249,140 +205,112 @@ describe('PulsePoint Adapter Tests', function () { expect(bid.ad).to.equal('This is an Ad'); expect(bid.width).to.equal(300); expect(bid.height).to.equal(250); - expect(bid.adId).to.equal('bid12345'); expect(bid.creative_id).to.equal('Creative#123'); expect(bid.creativeId).to.equal('Creative#123'); expect(bid.netRevenue).to.equal(true); expect(bid.currency).to.equal('USD'); expect(bid.ttl).to.equal(20); - }); - - it('Verify ttl/currency/adomain applied to bid', function () { - const request = spec.buildRequests(slotConfigs, bidderRequest); - const ortbRequest = request.data; - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: 'This is an Ad#1', - crid: 'Creative#123', - exp: 50, - adomain: ['advertiser.com'] - }, { - impid: ortbRequest.imp[1].id, - price: 1.25, - adm: 'This is an Ad#2', - crid: 'Creative#123' - }] - }], - cur: 'GBP' - }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); - expect(bids).to.have.lengthOf(2); - // verify first bid - const bid = bids[0]; - expect(bid.cpm).to.equal(1.25); - expect(bid.ad).to.equal('This is an Ad#1'); - expect(bid.ttl).to.equal(50); - expect(bid.currency).to.equal('GBP'); expect(bid.meta).to.not.be.null; expect(bid.meta.advertiserDomains).to.eql(['advertiser.com']); - const secondBid = bids[1]; - expect(secondBid.cpm).to.equal(1.25); - expect(secondBid.ad).to.equal('This is an Ad#2'); - expect(secondBid.ttl).to.equal(20); - expect(secondBid.currency).to.equal('GBP'); - expect(secondBid.meta).to.not.be.null; - expect(secondBid.meta.advertiserDomains).to.eql([]); }); it('Verify full passback', function () { const request = spec.buildRequests(slotConfigs, bidderRequest); - const bids = spec.interpretResponse({ body: null }, request) + const bids = spec.interpretResponse({body: null}, request) expect(bids).to.have.lengthOf(0); }); - it('Verify Native request', function () { - const request = spec.buildRequests(nativeSlotConfig, bidderRequest); - expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); - expect(request.method).to.equal('POST'); - const ortbRequest = request.data; - // native impression - expect(ortbRequest.imp[0].tagid).to.equal('t10000'); - expect(ortbRequest.imp[0].banner).to.equal(null); - const nativePart = ortbRequest.imp[0]['native']; - expect(nativePart).to.not.equal(null); - expect(nativePart.ver).to.equal('1.1'); - expect(nativePart.request).to.not.equal(null); - // native request assets - const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); - expect(nativeRequest).to.not.equal(null); - expect(nativeRequest.assets).to.have.lengthOf(3); - // title asset - expect(nativeRequest.assets[0].id).to.equal(1); - expect(nativeRequest.assets[0].required).to.equal(1); - expect(nativeRequest.assets[0].title).to.not.equal(null); - expect(nativeRequest.assets[0].title.len).to.equal(200); - // data asset - expect(nativeRequest.assets[1].id).to.equal(2); - expect(nativeRequest.assets[1].required).to.equal(0); - expect(nativeRequest.assets[1].title).to.be.undefined; - expect(nativeRequest.assets[1].data).to.not.equal(null); - expect(nativeRequest.assets[1].data.type).to.equal(1); - expect(nativeRequest.assets[1].data.len).to.equal(50); - // image asset - expect(nativeRequest.assets[2].id).to.equal(3); - expect(nativeRequest.assets[2].required).to.equal(0); - expect(nativeRequest.assets[2].title).to.be.undefined; - expect(nativeRequest.assets[2].img).to.not.equal(null); - expect(nativeRequest.assets[2].img.wmin).to.equal(100); - expect(nativeRequest.assets[2].img.hmin).to.equal(150); - expect(nativeRequest.assets[2].img.type).to.equal(3); - }); + if (FEATURES.NATIVE) { + it('Verify Native request', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // native impression + expect(ortbRequest.imp[0].tagid).to.equal('t10000'); + expect(ortbRequest.imp[0].banner).to.be.undefined; + const nativePart = ortbRequest.imp[0]['native']; + expect(nativePart).to.not.equal(null); + expect(nativePart.request).to.not.equal(null); + // native request assets + const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); + expect(nativeRequest).to.not.equal(null); + expect(nativeRequest.assets).to.have.lengthOf(3); + // image asset + expect(nativeRequest.assets[0].id).to.equal(1); + expect(nativeRequest.assets[0].required).to.equal(1); + expect(nativeRequest.assets[0].title).to.be.undefined; + expect(nativeRequest.assets[0].img).to.not.equal(null); + expect(nativeRequest.assets[0].img.w).to.equal(150); + expect(nativeRequest.assets[0].img.h).to.equal(50); + expect(nativeRequest.assets[0].img.type).to.equal(3); + // title asset + expect(nativeRequest.assets[1].id).to.equal(2); + expect(nativeRequest.assets[1].required).to.equal(1); + expect(nativeRequest.assets[1].title).to.not.equal(null); + expect(nativeRequest.assets[1].title.len).to.equal(80); + // data asset + expect(nativeRequest.assets[2].id).to.equal(3); + expect(nativeRequest.assets[2].required).to.equal(0); + expect(nativeRequest.assets[2].title).to.be.undefined; + expect(nativeRequest.assets[2].data).to.not.equal(null); + expect(nativeRequest.assets[2].data.type).to.equal(1); + }); - it('Verify Native response', function () { - const request = spec.buildRequests(nativeSlotConfig, bidderRequest); - expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); - expect(request.method).to.equal('POST'); - const ortbRequest = request.data; - const nativeResponse = { - 'native': { + it('Verify Native response', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + const nativeResponse = { assets: [ - { title: { text: 'Ad Title' } }, - { data: { type: 1, value: 'Sponsored By: Brand' } }, - { img: { type: 3, url: 'https://images.cdn.brand.com/123' } } + {id: 1, img: {type: 3, url: 'https://images.cdn.brand.com/123'}}, + {id: 2, title: {text: 'Ad Title'}}, + {id: 3, data: {type: 1, value: 'Sponsored By: Brand'}} ], - link: { url: 'https://brand.clickme.com/' }, + link: {url: 'https://brand.clickme.com/'}, imptrackers: ['https://imp1.trackme.com/', 'https://imp1.contextweb.com/'] - } - }; - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: JSON.stringify(nativeResponse) + + }; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: JSON.stringify(nativeResponse), + mtype: 4 + }] }] - }] - }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); - // verify bid - const bid = bids[0]; - expect(bid.cpm).to.equal(1.25); - expect(bid.adId).to.equal('bid12345'); - expect(bid.ad).to.be.undefined; - expect(bid.mediaType).to.equal('native'); - const nativeBid = bid['native']; - expect(nativeBid).to.not.equal(null); - expect(nativeBid.title).to.equal('Ad Title'); - expect(nativeBid.sponsoredBy).to.equal('Sponsored By: Brand'); - expect(nativeBid.image).to.equal('https://images.cdn.brand.com/123'); - expect(nativeBid.clickUrl).to.equal(encodeURIComponent('https://brand.clickme.com/')); - expect(nativeBid.impressionTrackers).to.have.lengthOf(2); - expect(nativeBid.impressionTrackers[0]).to.equal('https://imp1.trackme.com/'); - expect(nativeBid.impressionTrackers[1]).to.equal('https://imp1.contextweb.com/'); - }); + }; + const bids = spec.interpretResponse({body: ortbResponse}, request); + // verify bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.requestId).to.equal('bid12345'); + expect(bid.ad).to.be.undefined; + expect(bid.mediaType).to.equal('native'); + expect(bid['native']).to.not.be.null; + expect(bid['native'].ortb).to.not.be.null; + const nativeBid = bid['native'].ortb; + expect(nativeBid.assets).to.have.lengthOf(3); + expect(nativeBid.assets[0].id).to.equal(1); + expect(nativeBid.assets[0].img).to.not.be.null; + expect(nativeBid.assets[0].img.type).to.equal(3); + expect(nativeBid.assets[0].img.url).to.equal('https://images.cdn.brand.com/123'); + expect(nativeBid.assets[1].id).to.equal(2); + expect(nativeBid.assets[1].title).to.not.be.null; + expect(nativeBid.assets[1].title.text).to.equal('Ad Title'); + expect(nativeBid.assets[2].id).to.equal(3); + expect(nativeBid.assets[2].data).to.not.be.null; + expect(nativeBid.assets[2].data.type).to.equal(1); + expect(nativeBid.assets[2].data.value).to.equal('Sponsored By: Brand'); + expect(nativeBid.link).to.not.be.null; + expect(nativeBid.link.url).to.equal('https://brand.clickme.com/'); + expect(nativeBid.imptrackers).to.have.lengthOf(2); + expect(nativeBid.imptrackers[0]).to.equal('https://imp1.trackme.com/'); + expect(nativeBid.imptrackers[1]).to.equal('https://imp1.contextweb.com/'); + }); + } it('Verifies bidder code', function () { expect(spec.code).to.equal('pulsepoint'); @@ -430,19 +358,6 @@ describe('PulsePoint Adapter Tests', function () { expect(options[0].url).to.equal('https://bh.contextweb.com/visitormatch/prebid'); }); - it('Verify app requests', function () { - const request = spec.buildRequests(appSlotConfig, bidderRequest); - const ortbRequest = request.data; - // site object - expect(ortbRequest.site).to.equal(null); - expect(ortbRequest.app).to.not.be.null; - expect(ortbRequest.app.publisher).to.not.equal(null); - expect(ortbRequest.app.publisher.id).to.equal('p10000'); - expect(ortbRequest.app.bundle).to.equal('com.pulsepoint.apps'); - expect(ortbRequest.app.storeurl).to.equal('https://pulsepoint.com/apps'); - expect(ortbRequest.app.domain).to.equal('pulsepoint.com'); - }); - it('Verify GDPR', function () { const bidderRequestGdpr = { gdprConsent: { @@ -450,7 +365,7 @@ describe('PulsePoint Adapter Tests', function () { consentString: 'serialized_gpdr_data' } }; - const request = spec.buildRequests(slotConfigs, Object.assign({}, bidderRequest, bidderRequestGdpr)); + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(Object.assign({}, bidderRequest, bidderRequestGdpr))); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -468,7 +383,8 @@ describe('PulsePoint Adapter Tests', function () { const bidderRequestUSPrivacy = { uspConsent: '1YYY' }; - const request = spec.buildRequests(slotConfigs, Object.assign({}, bidderRequest, bidderRequestUSPrivacy)); + const request = spec.buildRequests(slotConfigs, + syncAddFPDToBidderRequest(Object.assign({}, bidderRequest, bidderRequestUSPrivacy))); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -478,52 +394,54 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.regs.ext.us_privacy).to.equal('1YYY'); }); - it('Verify Video request', function () { - const request = spec.buildRequests(videoSlotConfig, bidderRequest); - expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); - expect(request.method).to.equal('POST'); - const ortbRequest = request.data; - expect(ortbRequest).to.not.equal(null); - expect(ortbRequest.imp).to.have.lengthOf(1); - expect(ortbRequest.imp[0].video).to.not.be.null; - expect(ortbRequest.imp[0].native).to.be.null; - expect(ortbRequest.imp[0].banner).to.be.null; - expect(ortbRequest.imp[0].video.w).to.equal(400); - expect(ortbRequest.imp[0].video.h).to.equal(300); - expect(ortbRequest.imp[0].video.minduration).to.equal(5); - expect(ortbRequest.imp[0].video.maxduration).to.equal(10); - expect(ortbRequest.imp[0].video.startdelay).to.equal(0); - expect(ortbRequest.imp[0].video.skip).to.equal(1); - expect(ortbRequest.imp[0].video.minbitrate).to.equal(200); - expect(ortbRequest.imp[0].video.protocols).to.eql([1, 2, 4]); - }); + if (FEATURES.VIDEO) { + it('Verify Video request', function () { + const request = spec.buildRequests(videoSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].video).to.not.be.null; + expect(ortbRequest.imp[0].native).to.be.undefined; + expect(ortbRequest.imp[0].banner).to.be.undefined; + expect(ortbRequest.imp[0].video.w).to.equal(400); + expect(ortbRequest.imp[0].video.h).to.equal(300); + expect(ortbRequest.imp[0].video.minduration).to.equal(5); + expect(ortbRequest.imp[0].video.maxduration).to.equal(10); + expect(ortbRequest.imp[0].video.startdelay).to.equal(0); + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.minbitrate).to.equal(200); + expect(ortbRequest.imp[0].video.protocols).to.eql([1, 2, 4]); + }); - it('Verify Video response', function () { - const request = spec.buildRequests(videoSlotConfig, bidderRequest); - expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); - expect(request.method).to.equal('POST'); - const ortbRequest = request.data; - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: 'https//pulsepoint.video.mp4' + it('Verify Video response', function () { + const request = spec.buildRequests(videoSlotConfig, bidderRequest); + expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'https//pulsepoint.video.mp4', + mtype: 2 + }] }] - }] - }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); - const bid = bids[0]; - expect(bid.cpm).to.equal(1.25); - expect(bid.adId).to.equal('bid12345'); - expect(bid.ad).to.be.undefined; - expect(bid['native']).to.be.undefined; - expect(bid.mediaType).to.equal('video'); - expect(bid.vastXml).to.equal(ortbResponse.seatbid[0].bid[0].adm); - }); + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.ad).to.be.undefined; + expect(bid['native']).to.be.undefined; + expect(bid.mediaType).to.equal('video'); + expect(bid.vastXml).to.equal(ortbResponse.seatbid[0].bid[0].adm); + }); + } it('Verify extra parameters', function () { - let request = spec.buildRequests(additionalParamsConfig, bidderRequest); + let request = spec.buildRequests(additionalParamsConfig, syncAddFPDToBidderRequest(bidderRequest)); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.imp).to.have.lengthOf(1); @@ -538,31 +456,15 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.imp[0].ext.prebid.extra_key4).to.eql([1, 2, 3]); expect(Object.keys(ortbRequest.imp[0].ext.prebid)).to.eql(['extra_key1', 'extra_key2', 'extra_key3', 'extra_key4']); // attempting with a configuration with no unknown params. - request = spec.buildRequests(outstreamSlotConfig, bidderRequest); + request = spec.buildRequests(videoSlotConfig, bidderRequest); ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.imp).to.have.lengthOf(1); - expect(ortbRequest.imp[0].ext).to.equal(null); - }); - - it('Verify ortb parameters', function () { - const request = spec.buildRequests(ortbParamsSlotConfig, bidderRequest); - const ortbRequest = request.data; - expect(ortbRequest).to.not.equal(null); - expect(ortbRequest.bcat).to.eql(['IAB-1', 'IAB-20']); - expect(ortbRequest.badv).to.eql(['cocacola.com', 'lays.com']); - expect(ortbRequest.imp).to.have.lengthOf(2); - expect(ortbRequest.imp[0].bidfloor).to.equal(1.5); - expect(ortbRequest.imp[0].banner.battr).to.eql([1, 2, 3]); - expect(ortbRequest.imp[0].ext).to.be.null; - // slot 2 - expect(ortbRequest.imp[1].bidfloor).to.equal(2.5); - expect(ortbRequest.imp[1].video.battr).to.eql([2, 3, 4]); - expect(ortbRequest.imp[1].ext).to.be.null; + expect(ortbRequest.imp[0].ext).to.be.undefined; }); it('Verify schain parameters', function () { - const request = spec.buildRequests(schainParamsSlotConfig, bidderRequest); + const request = spec.buildRequests(schainParamsSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.source).to.not.equal(null); @@ -580,42 +482,6 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.source.ext.schain.nodes[0].domain).to.equal('publisher.com'); }); - it('Verify outstream renderer', function () { - const bidderRequestOutstream = Object.assign({}, bidderRequest, {bids: [outstreamSlotConfig[0]]}); - const request = spec.buildRequests(outstreamSlotConfig, bidderRequestOutstream); - const ortbRequest = request.data; - expect(ortbRequest).to.not.be.null; - expect(ortbRequest.imp[0]).to.not.be.null; - expect(ortbRequest.imp[0].video).to.not.be.null; - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: 'https//pulsepoint.video.mp4', - ext: { - outstream: { - type: 'Inline', - config: { - text: 'ADVERTISEMENT', - skipaftersec: 5 - }, - rendererUrl: 'https://tag.contextweb.com/hb-outstr-renderer.js' - } - } - }] - }] - }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); - const bid = bids[0]; - expect(bid.cpm).to.equal(1.25); - expect(bid.renderer).to.not.be.null; - expect(bid.renderer.url).to.equal('https://tag.contextweb.com/hb-outstr-renderer.js'); - expect(bid.renderer.getConfig()).to.not.be.null; - expect(bid.renderer.getConfig().defaultOptions).to.eql(ortbResponse.seatbid[0].bid[0].ext.outstream.config); - expect(bid.renderer.getConfig().rendererOptions).to.eql(outstreamSlotConfig[0].renderer.options); - expect(bid.renderer.getConfig().type).to.equal('Inline'); - }); it('Verify common id parameters', function () { const bidRequests = deepClone(slotConfigs); bidRequests[0].userIdAsEids = [{ @@ -633,169 +499,17 @@ describe('PulsePoint Adapter Tests', function () { }] } ]; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); expect(request).to.be.not.null; - const ortbRequest = request.data; expect(request.data).to.be.not.null; + const ortbRequest = request.data; // user object expect(ortbRequest.user).to.not.be.undefined; expect(ortbRequest.user.ext).to.not.be.undefined; expect(ortbRequest.user.ext.eids).to.not.be.undefined; expect(ortbRequest.user.ext.eids).to.deep.equal(bidRequests[0].userIdAsEids); }); - it('Verify multiple adsizes', function () { - const bidRequests = deepClone(slotConfigs); - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request).to.be.not.null; - expect(request.data).to.be.not.null; - const ortbRequest = request.data; - expect(ortbRequest.imp).to.have.lengthOf(2); - // first impression has multi sizes - expect(ortbRequest.imp[0].banner).to.not.be.null; - expect(ortbRequest.imp[0].banner.w).to.equal(300); - expect(ortbRequest.imp[0].banner.h).to.equal(250); - expect(ortbRequest.imp[0].banner.format).to.not.be.null; - expect(ortbRequest.imp[0].banner.format).to.have.lengthOf(2); - expect(ortbRequest.imp[0].banner.format[0].w).to.equal(728); - expect(ortbRequest.imp[0].banner.format[0].h).to.equal(90); - expect(ortbRequest.imp[0].banner.format[1].w).to.equal(160); - expect(ortbRequest.imp[0].banner.format[1].h).to.equal(600); - // slot 2 - expect(ortbRequest.imp[1].banner).to.not.be.null; - expect(ortbRequest.imp[1].banner.w).to.equal(728); - expect(ortbRequest.imp[1].banner.h).to.equal(90); - expect(ortbRequest.imp[1].banner.format).to.not.be.null; - expect(ortbRequest.imp[1].banner.format).to.have.lengthOf(1); - expect(ortbRequest.imp[1].banner.format[0].w).to.equal(728); - expect(ortbRequest.imp[1].banner.format[0].h).to.equal(90); - // adsize on response - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: 'This is an Ad', - crid: 'Creative#123', - w: 728, - h: 90 - }] - }] - }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); - expect(bids).to.have.lengthOf(1); - const bid = bids[0]; - expect(bid.width).to.equal(728); - expect(bid.height).to.equal(90); - }); - it('Verify multi-format response', function () { - const bidRequests = deepClone(slotConfigs); - bidRequests[0].mediaTypes['native'] = { - title: { - required: true - }, - image: { - required: true - }, - sponsoredBy: { - required: true - } - }; - bidRequests[1].params.video = { - w: 400, - h: 300, - minduration: 5, - maxduration: 10, - }; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request).to.be.not.null; - expect(request.data).to.be.not.null; - const ortbRequest = request.data; - expect(ortbRequest.imp).to.have.lengthOf(2); - // adsize on response - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: 'This is an Ad', - crid: 'Creative#123', - w: 728, - h: 90 - }, { - impid: ortbRequest.imp[1].id, - price: 2.5, - adm: '', - crid: 'Creative#234', - w: 728, - h: 90 - }] - }] - }; - // request has both types - banner and native, response is parsed as banner. - // for impression#2, response is parsed as video - const bids = spec.interpretResponse({ body: ortbResponse }, request); - expect(bids).to.have.lengthOf(2); - const bid = bids[0]; - expect(bid.width).to.equal(728); - expect(bid.height).to.equal(90); - const secondBid = bids[1]; - expect(secondBid.vastXml).to.equal(''); - }); - it('Verify bid floor', function () { - const bidRequests = deepClone(slotConfigs); - bidRequests[0].params.bidfloor = 1.05; - let request = spec.buildRequests(bidRequests, bidderRequest); - let ortbRequest = request.data; - expect(ortbRequest).to.not.equal(null); - expect(ortbRequest.imp[0].bidfloor).to.equal(1.05); - expect(ortbRequest.imp[1].bidfloor).to.be.undefined; - let floorArg = null; - // publisher uses the floor module - bidRequests[0].getFloor = (arg) => { - floorArg = arg; - return { floor: 1.25 }; - }; - bidRequests[1].getFloor = () => { - return { floor: 2.05 }; - }; - request = spec.buildRequests(bidRequests, bidderRequest); - ortbRequest = request.data; - expect(ortbRequest).to.not.equal(null); - expect(ortbRequest.imp[0].bidfloor).to.equal(1.25); - expect(ortbRequest.imp[1].bidfloor).to.equal(2.05); - expect(floorArg).to.not.be.null; - expect(floorArg.mediaType).to.equal('banner'); - expect(floorArg.currency).to.equal('USD'); - expect(floorArg.size).to.equal('*'); - }); - it('Verify Video params on mediaTypes.video', function () { - const bidRequests = deepClone(videoSlotConfig); - bidRequests[0].mediaTypes = { - video: { - w: 600, - h: 400, - minduration: 15, - maxduration: 20, - startdelay: 10, - skip: 0, - } - }; - const request = spec.buildRequests(bidRequests, bidderRequest); - const ortbRequest = request.data; - expect(ortbRequest).to.not.equal(null); - expect(ortbRequest.imp).to.have.lengthOf(1); - expect(ortbRequest.imp[0].video).to.not.be.null; - expect(ortbRequest.imp[0].native).to.be.null; - expect(ortbRequest.imp[0].banner).to.be.null; - expect(ortbRequest.imp[0].video.w).to.equal(600); - expect(ortbRequest.imp[0].video.h).to.equal(400); - expect(ortbRequest.imp[0].video.minduration).to.equal(15); - expect(ortbRequest.imp[0].video.maxduration).to.equal(20); - expect(ortbRequest.imp[0].video.startdelay).to.equal(10); - expect(ortbRequest.imp[0].video.skip).to.equal(0); - expect(ortbRequest.imp[0].video.minbitrate).to.equal(200); - expect(ortbRequest.imp[0].video.protocols).to.eql([1, 2, 4]); - }); + it('Verify user level first party data', function () { const bidderRequest = { refererInfo: { @@ -819,7 +533,7 @@ describe('PulsePoint Adapter Tests', function () { } } }; - let request = spec.buildRequests(slotConfigs, bidderRequest); + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.user).to.not.equal(null); @@ -835,12 +549,9 @@ describe('PulsePoint Adapter Tests', function () { } }); }); + it('Verify site level first party data', function () { const bidderRequest = { - refererInfo: { - page: 'https://publisher.com/home', - ref: 'https://referrer' - }, ortb2: { site: { content: { @@ -853,11 +564,14 @@ describe('PulsePoint Adapter Tests', function () { }] }, page: 'http://pub.com/news', - ref: 'http://google.com' + ref: 'http://google.com', + publisher: { + domain: 'pub.com' + } } } }; - let request = spec.buildRequests(slotConfigs, bidderRequest); + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.site).to.not.equal(null); @@ -871,13 +585,15 @@ describe('PulsePoint Adapter Tests', function () { } }] }, - page: 'https://publisher.com/home', - ref: 'https://referrer', + page: 'http://pub.com/news', + ref: 'http://google.com', publisher: { - id: 'p10000' + id: 'p10000', + domain: 'pub.com' } }); }); + it('Verify impression/slot level first party data', function () { const bidderRequests = [{ placementCode: '/DfpAccount1/slot1', @@ -928,4 +644,27 @@ describe('PulsePoint Adapter Tests', function () { // assert bidderRequest value is used when available expect(mkRequest(Object.assign({}, { timeout: 6000 }, bidderRequest)).tmax).to.equal(6000) }); + + it('Verify deals', function () { + const bidRequests = deepClone(slotConfigs); + const deals = [{ + id: 'DEAL_ONE', + bidfloor: 1.1 + }, { + id: 'DEAL_TWO', + bidfloor: 2.2 + }]; + bidRequests[0].params.deals = deals; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // slot 1 + expect(ortbRequest.imp[0].tagid).to.equal('t10000'); + expect(ortbRequest.imp[0].pmp).to.not.be.undefined; + expect(ortbRequest.imp[0].pmp).to.deep.equal({ + private_auction: 0, + deals + }); + }) }); diff --git a/test/spec/modules/qortexRtdProvider_spec.js b/test/spec/modules/qortexRtdProvider_spec.js new file mode 100644 index 00000000000..9baa526e4cc --- /dev/null +++ b/test/spec/modules/qortexRtdProvider_spec.js @@ -0,0 +1,333 @@ +import * as utils from 'src/utils'; +import * as ajax from 'src/ajax.js'; +import * as events from 'src/events.js'; +import CONSTANTS from '../../../src/constants.json'; +import {loadExternalScript} from 'src/adloader.js'; +import { + qortexSubmodule as module, + getContext, + addContextToRequests, + setContextData, + initializeModuleData, + loadScriptTag +} from '../../../modules/qortexRtdProvider'; +import {server} from '../../mocks/xhr.js'; +import { cloneDeep } from 'lodash'; + +describe('qortexRtdProvider', () => { + let logWarnSpy; + let ortb2Stub; + + const defaultApiHost = 'https://demand.qortex.ai'; + const defaultGroupId = 'test'; + + const validBidderArray = ['qortex', 'test']; + const validTagConfig = { + videoContainer: 'my-video-container' + } + + const validModuleConfig = { + params: { + groupId: defaultGroupId, + apiUrl: defaultApiHost, + bidders: validBidderArray + } + }, + emptyModuleConfig = { + params: {} + } + + const validImpressionEvent = { + detail: { + uid: 'uid123', + type: 'qx-impression' + } + }, + validImpressionEvent2 = { + detail: { + uid: 'uid1234', + type: 'qx-impression' + } + }, + missingIdImpressionEvent = { + detail: { + type: 'qx-impression' + } + }, + invalidTypeQortexEvent = { + detail: { + type: 'invalid-type' + } + } + + const responseHeaders = { + 'content-type': 'application/json', + 'access-control-allow-origin': '*' + }; + + const responseObj = { + content: { + id: '123456', + episode: 15, + title: 'test episode', + series: 'test show', + season: '1', + url: 'https://example.com/file.mp4' + } + }; + + const apiResponse = JSON.stringify(responseObj); + + const reqBidsConfig = { + adUnits: [{ + bids: [ + { bidder: 'qortex' } + ] + }], + ortb2Fragments: { + bidder: {}, + global: {} + } + } + + beforeEach(() => { + ortb2Stub = sinon.stub(reqBidsConfig, 'ortb2Fragments').value({bidder: {}, global: {}}) + logWarnSpy = sinon.spy(utils, 'logWarn'); + }) + + afterEach(() => { + logWarnSpy.restore(); + ortb2Stub.restore(); + setContextData(null); + }) + + describe('init', () => { + it('returns true for valid config object', () => { + expect(module.init(validModuleConfig)).to.be.true; + }) + + it('returns false and logs error for missing groupId', () => { + expect(module.init(emptyModuleConfig)).to.be.false; + expect(logWarnSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('Qortex RTD module config does not contain valid groupId parameter. Config params: {}')).to.be.ok; + }) + + it('loads Qortex script if tagConfig is present in module config params', () => { + const config = cloneDeep(validModuleConfig); + config.params.tagConfig = validTagConfig; + expect(module.init(config)).to.be.true; + expect(loadExternalScript.calledOnce).to.be.true; + }) + }) + + describe('loadScriptTag', () => { + let addEventListenerSpy; + let billableEvents = []; + + let config = cloneDeep(validModuleConfig); + config.params.tagConfig = validTagConfig; + + events.on(CONSTANTS.EVENTS.BILLABLE_EVENT, (e) => { + billableEvents.push(e); + }) + + beforeEach(() => { + initializeModuleData(config); + addEventListenerSpy = sinon.spy(window, 'addEventListener'); + }) + + afterEach(() => { + addEventListenerSpy.restore(); + billableEvents = []; + }) + + it('adds event listener', () => { + loadScriptTag(config); + expect(addEventListenerSpy.calledOnce).to.be.true; + }) + + it('parses incoming qortex-impression events', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + expect(billableEvents.length).to.be.equal(1); + expect(billableEvents[0].type).to.be.equal(validImpressionEvent.detail.type); + expect(billableEvents[0].transactionId).to.be.equal(validImpressionEvent.detail.uid); + }) + + it('will emit two events for impressions with two different ids', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent2)); + expect(billableEvents.length).to.be.equal(2); + expect(billableEvents[0].transactionId).to.be.equal(validImpressionEvent.detail.uid); + expect(billableEvents[1].transactionId).to.be.equal(validImpressionEvent2.detail.uid); + }) + + it('will not allow multiple events with the same id', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + expect(billableEvents.length).to.be.equal(1); + expect(logWarnSpy.calledWith('received invalid billable event due to duplicate uid: qx-impression')).to.be.ok; + }) + + it('will not allow events with missing uid', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', missingIdImpressionEvent)); + expect(billableEvents.length).to.be.equal(0); + expect(logWarnSpy.calledWith('received invalid billable event due to missing uid: qx-impression')).to.be.ok; + }) + + it('will not allow events with unavailable type', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', invalidTypeQortexEvent)); + expect(billableEvents.length).to.be.equal(0); + expect(logWarnSpy.calledWith('received invalid billable event: invalid-type')).to.be.ok; + }) + }) + + describe('getBidRequestData', () => { + let callbackSpy; + + beforeEach(() => { + initializeModuleData(validModuleConfig); + callbackSpy = sinon.spy(); + }) + + afterEach(() => { + initializeModuleData(emptyModuleConfig); + callbackSpy.resetHistory(); + }) + + it('will call callback immediately if no adunits', () => { + const reqBidsConfigNoBids = { adUnits: [] }; + module.getBidRequestData(reqBidsConfigNoBids, callbackSpy); + expect(callbackSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfigNoBids))).to.be.ok; + }) + + it('will call callback if getContext does not throw', () => { + const cb = function () { + expect(logWarnSpy.calledOnce).to.be.false; + done(); + } + module.getBidRequestData(reqBidsConfig, cb); + server.requests[0].respond(200, responseHeaders, apiResponse); + }) + + it('will catch and log error and fire callback', (done) => { + const a = sinon.stub(ajax, 'ajax').throws(new Error('test')); + const cb = function () { + expect(logWarnSpy.calledWith('test')).to.be.eql(true); + done(); + } + module.getBidRequestData(reqBidsConfig, cb); + a.restore(); + }) + }) + + describe('getContext', () => { + beforeEach(() => { + initializeModuleData(validModuleConfig); + }) + + afterEach(() => { + initializeModuleData(emptyModuleConfig); + }) + + it('returns a promise', (done) => { + const result = getContext(); + expect(result).to.be.a('promise'); + done(); + }) + + it('uses request url generated from initialize function in config and resolves to content object data', (done) => { + let requestUrl = `${validModuleConfig.params.apiUrl}/api/v1/analyze/${validModuleConfig.params.groupId}/prebid`; + const ctx = getContext() + expect(server.requests.length).to.be.eql(1); + expect(server.requests[0].url).to.be.eql(requestUrl); + server.requests[0].respond(200, responseHeaders, apiResponse); + ctx.then(response => { + expect(response).to.be.eql(responseObj.content); + done(); + }); + }) + + it('will return existing context data instead of ajax call if the source was not updated', (done) => { + setContextData(responseObj.content); + const ctx = getContext(); + expect(server.requests.length).to.be.eql(0); + ctx.then(response => { + expect(response).to.be.eql(responseObj.content); + done(); + }); + }) + + it('returns null for non erroring api responses other than 200', (done) => { + const nullContentResponse = { content: null } + const ctx = getContext() + server.requests[0].respond(200, responseHeaders, JSON.stringify(nullContentResponse)) + ctx.then(response => { + expect(response).to.be.null; + expect(server.requests.length).to.be.eql(1); + expect(logWarnSpy.called).to.be.false; + done(); + }); + }) + }) + + describe(' addContextToRequests', () => { + it('logs error if no data was retrieved from get context call', () => { + initializeModuleData(validModuleConfig); + addContextToRequests(reqBidsConfig); + expect(logWarnSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('No context data received at this time')).to.be.ok; + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + + it('adds site.content only to global ortb2 when bidders array is omitted', () => { + const omittedBidderArrayConfig = cloneDeep(validModuleConfig); + delete omittedBidderArrayConfig.params.bidders; + initializeModuleData(omittedBidderArrayConfig); + setContextData(responseObj.content); + addContextToRequests(reqBidsConfig); + expect(reqBidsConfig.ortb2Fragments.global).to.have.property('site'); + expect(reqBidsConfig.ortb2Fragments.global.site).to.have.property('content'); + expect(reqBidsConfig.ortb2Fragments.global.site.content).to.be.eql(responseObj.content); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + + it('adds site.content only to bidder ortb2 when bidders array is included', () => { + initializeModuleData(validModuleConfig); + setContextData(responseObj.content); + addContextToRequests(reqBidsConfig); + + const qortexOrtb2Fragment = reqBidsConfig.ortb2Fragments.bidder['qortex'] + expect(qortexOrtb2Fragment).to.not.be.null; + expect(qortexOrtb2Fragment).to.have.property('site'); + expect(qortexOrtb2Fragment.site).to.have.property('content'); + expect(qortexOrtb2Fragment.site.content).to.be.eql(responseObj.content); + + const testOrtb2Fragment = reqBidsConfig.ortb2Fragments.bidder['test'] + expect(testOrtb2Fragment).to.not.be.null; + expect(testOrtb2Fragment).to.have.property('site'); + expect(testOrtb2Fragment.site).to.have.property('content'); + expect(testOrtb2Fragment.site.content).to.be.eql(responseObj.content); + + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + }) + + it('logs error if there is an empty bidder array', () => { + const invalidBidderArrayConfig = cloneDeep(validModuleConfig); + invalidBidderArrayConfig.params.bidders = []; + initializeModuleData(invalidBidderArrayConfig); + setContextData(responseObj.content) + addContextToRequests(reqBidsConfig); + + expect(logWarnSpy.calledWith('Config contains an empty bidders array, unable to determine which bids to enrich')).to.be.ok; + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + }) +}) diff --git a/test/spec/modules/rasBidAdapter_spec.js b/test/spec/modules/rasBidAdapter_spec.js index bfa72a2510e..719e15ad695 100644 --- a/test/spec/modules/rasBidAdapter_spec.js +++ b/test/spec/modules/rasBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/rasBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import {getAdUnitSizes} from '../../../src/utils'; const CSR_ENDPOINT = 'https://csr.onet.pl/4178463/csr-006/csr.json?nid=4178463&'; @@ -192,5 +193,56 @@ describe('rasBidAdapter', function () { const resp = spec.interpretResponse({ body: res }, {}); expect(resp).to.deep.equal([]); }); + + it('should generate auctionConfig when fledge is enabled', function () { + let bidRequest = { + method: 'GET', + url: 'https://example.com', + bidIds: [{ + slot: 'top', + bidId: '123', + network: 'testnetwork', + sizes: ['300x250'], + params: { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + fledgeEnabled: true + }, + { + slot: 'top', + bidId: '456', + network: 'testnetwork', + sizes: ['300x250'], + params: { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + fledgeEnabled: false + }] + }; + + let auctionConfigs = [{ + 'bidId': '123', + 'config': { + 'seller': 'https://csr.onet.pl', + 'decisionLogicUrl': 'https://csr.onet.pl/testnetwork/v1/protected-audience-api/decision-logic.js', + 'interestGroupBuyers': ['https://csr.onet.pl'], + 'auctionSignals': { + 'params': { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + 'sizes': ['300x250'], + 'gctx': '1234567890' + } + } + }]; + const resp = spec.interpretResponse({body: {gctx: '1234567890'}}, bidRequest); + expect(resp).to.deep.equal({bids: [], fledgeAuctionConfigs: auctionConfigs}); + }); }); }); diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 36d03c01138..7778e9cbf80 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -7,9 +7,6 @@ import {getCoreStorageManager} from '../../../src/storageManager.js'; const UUID_KEY = 'relaido_uuid'; const relaido_uuid = 'hogehoge'; -const storage = getCoreStorageManager(); -storage.setCookie(UUID_KEY, relaido_uuid); - describe('RelaidoAdapter', function () { let bidRequest; let bidderRequest; @@ -18,6 +15,10 @@ describe('RelaidoAdapter', function () { let serverRequest; let generateUUIDStub; let triggerPixelStub; + before(() => { + const storage = getCoreStorageManager(); + storage.setCookie(UUID_KEY, relaido_uuid); + }); beforeEach(function () { generateUUIDStub = sinon.stub(utils, 'generateUUID').returns(relaido_uuid); diff --git a/test/spec/modules/relayBidAdapter_spec.js b/test/spec/modules/relayBidAdapter_spec.js new file mode 100644 index 00000000000..38a3cfc9b97 --- /dev/null +++ b/test/spec/modules/relayBidAdapter_spec.js @@ -0,0 +1,131 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/relayBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'relay' +const endpoint = 'https://e.relay.bid/p/openrtb2'; + +describe('RelayBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder, + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: { + accountId: 15000, + }, + ortb2Imp: { + ext: { + relay: { + bidders: { + bidderA: { + theId: 'abc123' + }, + bidderB: { + theId: 'xyz789' + } + } + } + } + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder, + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: { + accountId: 30000, + }, + ortb2Imp: { + ext: { + relay: { + bidders: { + bidderA: { + theId: 'def456' + }, + bidderB: { + theId: 'uvw101112' + } + } + } + } + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: {} + } + + const bidderRequest = {}; + + describe('isBidRequestValid', function () { + it('Valid bids have a params.accountId.', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Invalid bids do not have a params.accountId.', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + const requests = spec.buildRequests(bids, bidderRequest); + const firstRequest = requests[0]; + const secondRequest = requests[1]; + + it('Creates two requests', function () { + expect(firstRequest).to.exist; + expect(firstRequest.data).to.exist; + expect(firstRequest.method).to.exist; + expect(firstRequest.method).to.equal('POST'); + expect(firstRequest.url).to.exist; + expect(firstRequest.url).to.equal(`${endpoint}?a=15000&pb=1&pbv=v8.1.0`); + + expect(secondRequest).to.exist; + expect(secondRequest.data).to.exist; + expect(secondRequest.method).to.exist; + expect(secondRequest.method).to.equal('POST'); + expect(secondRequest.url).to.exist; + expect(secondRequest.url).to.equal(`${endpoint}?a=30000&pb=1&pbv=v8.1.0`); + }); + + it('Does not generate requests when there are no bids', function () { + const request = spec.buildRequests([], bidderRequest); + expect(request).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function () { + it('Uses Prebid consent values if incoming sync URLs lack consent.', function () { + const syncOpts = { + iframeEnabled: true, + pixelEnabled: true + }; + const test_gdpr_applies = true; + const test_gdpr_consent_str = 'TEST_GDPR_CONSENT_STRING'; + const responses = [{ + body: { + ext: { + user_syncs: [ + { type: 'image', url: 'https://image-example.com' }, + { type: 'iframe', url: 'https://iframe-example.com' } + ] + } + } + }]; + + const sync_urls = spec.getUserSyncs(syncOpts, responses, { gdprApplies: test_gdpr_applies, consentString: test_gdpr_consent_str }); + expect(sync_urls).to.be.an('array'); + expect(sync_urls[0].url).to.equal('https://image-example.com/?gdpr=1&gdpr_consent=TEST_GDPR_CONSENT_STRING'); + expect(sync_urls[1].url).to.equal('https://iframe-example.com/?gdpr=1&gdpr_consent=TEST_GDPR_CONSENT_STRING'); + }); + }); +}); diff --git a/test/spec/modules/relevantdigitalBidAdapter_spec.js b/test/spec/modules/relevantdigitalBidAdapter_spec.js index b2a5495b3cb..0e21453c8ba 100644 --- a/test/spec/modules/relevantdigitalBidAdapter_spec.js +++ b/test/spec/modules/relevantdigitalBidAdapter_spec.js @@ -1,5 +1,10 @@ import {spec, resetBidderConfigs} from 'modules/relevantdigitalBidAdapter.js'; import { parseUrl, deepClone } from 'src/utils.js'; +import { config } from 'src/config.js'; +import CONSTANTS from 'src/constants.json'; + +import adapterManager, { +} from 'src/adapterManager.js'; const expect = require('chai').expect; @@ -9,14 +14,29 @@ const ACCOUNT_ID = 'example_account_id'; const TEST_DOMAIN = 'example.com'; const TEST_PAGE = `https://${TEST_DOMAIN}/page.html`; -const BID_REQUEST = -{ - 'bidder': 'relevantdigital', +const CONFIG = { + enabled: true, + endpoint: CONSTANTS.S2S.DEFAULT_ENDPOINT, + timeout: 1000, + maxBids: 1, + adapter: 'prebidServer', + bidders: ['relevantdigital'], + accountId: 'abc' +}; + +const ADUNIT_CODE = '/19968336/header-bid-tag-0'; + +const BID_PARAMS = { 'params': { 'placementId': PLACEMENT_ID, 'accountId': ACCOUNT_ID, - 'pbsHost': PBS_HOST, - }, + 'pbsHost': PBS_HOST + } +}; + +const BID_REQUEST = { + 'bidder': 'relevantdigital', + ...BID_PARAMS, 'ortb2Imp': { 'ext': { 'tid': 'e13391ea-00f3-495d-99a6-d937990d73a9' @@ -32,7 +52,7 @@ const BID_REQUEST = ] } }, - 'adUnitCode': '/19968336/header-bid-tag-0', + 'adUnitCode': ADUNIT_CODE, 'transactionId': 'e13391ea-00f3-495d-99a6-d937990d73a9', 'sizes': [ [ @@ -292,4 +312,64 @@ describe('Relevant Digital Bid Adaper', function () { expect(allSyncs).to.deep.equal(expectedResult) }); }); + describe('transformBidParams', function () { + beforeEach(() => { + config.setConfig({ + s2sConfig: CONFIG, + }); + }); + afterEach(() => { + config.resetConfig(); + }); + + const adUnit = (params) => ({ + code: ADUNIT_CODE, + bids: [ + { + bidder: 'relevantdigital', + adUnitCode: ADUNIT_CODE, + params, + } + ] + }); + + const request = (params) => adapterManager.makeBidRequests([adUnit(params)], 123, 'auction-id', 123, [], {})[0]; + + it('transforms adunit bid params and config params correctly', function () { + config.setConfig({ + relevantdigital: { + pbsHost: PBS_HOST, + accountId: ACCOUNT_ID, + }, + }); + const adUnitParams = { placementId: PLACEMENT_ID }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: `https://${BID_PARAMS.params.pbsHost}`, 'pbsBufferMs': 250 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('transforms adunit bid params correctly', function () { + const adUnitParams = { ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('transforms adunit bid params correctly', function () { + const adUnitParams = { ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('does not transform bid params if placementId is missing', function () { + const adUnitParams = { ...BID_PARAMS.params, placementId: null }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.equal(undefined); + }); + it('does not transform bid params s2s config is missing', function () { + config.resetConfig(); + const adUnitParams = BID_PARAMS.params; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.equal(undefined); + }); + }) }); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index d9c3555bf03..3f3cbfbb46c 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -270,7 +270,81 @@ describe('the rubicon adapter', function () { }], criteoId: '1111', }; - bid.userIdAsEids = createEidsArray(bid.userId); + bid.userIdAsEids = [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '0000-1111-2222-3333', + 'atype': 3 + } + ], + 'ext': { + 'segments': [ + 'segA', + 'segB' + ] + } + }, + { + 'source': 'liveramp.com', + 'uids': [ + { + 'id': '1111-2222-3333-4444', + 'atype': 3 + } + ] + }, + { + 'source': 'adserver.org', + 'uids': [ + { + 'id': '3000', + 'atype': 1, + 'ext': { + 'rtiPartner': 'TDID' + } + } + ] + }, + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '4000', + 'atype': 1 + } + ] + }, + { + 'source': 'example.com', + 'uids': [ + { + 'id': '333333', + 'ext': { + 'stype': 'ppuid' + } + } + ] + }, + { + 'source': 'id-partner.com', + 'uids': [ + { + 'id': '4444444' + } + ] + }, + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': '1111', + 'atype': 1 + } + ] + } + ]; return bidderRequest; } @@ -622,6 +696,16 @@ describe('the rubicon adapter', function () { expect(data['p_pos']).to.equal('atf;;btf;;'); }); + it('should correctly send cdep signal when requested', () => { + var badposRequest = utils.deepClone(bidderRequest); + badposRequest.bids[0].ortb2 = {device: {ext: {cdep: 3}}}; + + let [request] = spec.buildRequests(badposRequest.bids, badposRequest); + let data = parseQuery(request.data); + + expect(data['o_cdep']).to.equal('3'); + }); + it('ad engine query params should be ordered correctly', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); @@ -825,6 +909,29 @@ describe('the rubicon adapter', function () { }); }); + describe('GPP Consent', function () { + it('should send gpp information if bidderRequest has a value for gppConsent', function () { + bidderRequest.gppConsent = { + gppString: 'consent', + applicableSections: 2 + }; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); + delete bidderRequest.gppConsent; + + expect(data['gpp']).to.equal('consent'); + expect(data['gpp_sid']).to.equal('2'); + }); + + it('should not send gpp information if bidderRequest does not have a value for gppConsent', function () { + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); + + expect(data['gpp']).to.equal(undefined); + expect(data['gpp_sid']).to.equal(undefined); + }); + }); + describe('first party data', function () { it('should not have any tg_v or tg_i params if all are undefined', function () { let params = { @@ -1132,6 +1239,30 @@ describe('the rubicon adapter', function () { }); }); + it('should still use single request if other rubicon configs are set after', function () { + // set single request to true + config.setConfig({ rubicon: { singleRequest: true } }); + + // execute some other rubicon setConfig + config.setConfig({ rubicon: { netRevenue: true } }); + + const bidCopy = utils.deepClone(bidderRequest.bids[0]); + bidderRequest.bids.push(bidCopy); + bidderRequest.bids.push(bidCopy); + bidderRequest.bids.push(bidCopy); + + let serverRequests = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // should have 1 request only + expect(serverRequests).that.is.an('array').of.length(1); + + // get the built query + let data = parseQuery(serverRequests[0].data); + + // num slots should be 4 + expect(data.slots).to.equal('4'); + }); + it('should not group bid requests if singleRequest does not equal true', function () { config.setConfig({rubicon: {singleRequest: false}}); @@ -1201,7 +1332,20 @@ describe('the rubicon adapter', function () { clonedBid.userId = { tdid: 'abcd-efgh-ijkl-mnop-1234' }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'adserver.org', + 'uids': [ + { + 'id': 'abcd-efgh-ijkl-mnop-1234', + 'atype': 1, + 'ext': { + 'rtiPartner': 'TDID' + } + } + ] + } + ]; let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1218,7 +1362,23 @@ describe('the rubicon adapter', function () { segments: ['segA', 'segB'] } }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '0000-1111-2222-3333', + 'atype': 3 + } + ], + 'ext': { + 'segments': [ + 'segA', + 'segB' + ] + } + } + ]; let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1235,7 +1395,23 @@ describe('the rubicon adapter', function () { segments: ['segD', 'segE'] } }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '1111-2222-3333-4444', + 'atype': 3 + } + ], + 'ext': { + 'segments': [ + 'segD', + 'segE' + ] + } + } + ] let [request] = spec.buildRequests([clonedBid], bidderRequest); const unescapedData = unescape(request.data); @@ -1250,7 +1426,17 @@ describe('the rubicon adapter', function () { clonedBid.userId = { idl_env: '1111-2222-3333-4444' }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'liveramp.com', + 'uids': [ + { + 'id': '1111-2222-3333-4444', + 'atype': 3 + } + ] + } + ] let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1264,7 +1450,17 @@ describe('the rubicon adapter', function () { clonedBid.userId = { pubcid: '1111' }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '1111', + 'atype': 1 + } + ] + } + ] let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1278,7 +1474,17 @@ describe('the rubicon adapter', function () { clonedBid.userId = { criteoId: '1111' }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': '1111', + 'atype': 1 + } + ] + } + ] let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1305,7 +1511,27 @@ describe('the rubicon adapter', function () { }] }] }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'example.com', + 'uids': [ + { + 'id': '11111', + 'ext': { + 'stype': 'ppuid' + } + } + ] + }, + { + 'source': 'id-partner.com', + 'uids': [ + { + 'id': '222222' + } + ] + } + ]; let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1324,7 +1550,20 @@ describe('the rubicon adapter', function () { } } }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': '11111', + 'atype': 1, + 'ext': { + 'linkType': '22222' + } + } + ] + } + ]; let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1348,6 +1587,22 @@ describe('the rubicon adapter', function () { expect(data['eid_catchall']).to.equal('11111^2'); }); + + it('should send rubiconproject special case', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + // Hardcoding userIdAsEids since createEidsArray returns empty array if source not found in eids.js + clonedBid.userIdAsEids = [{ + source: 'rubiconproject.com', + uids: [{ + id: 'some-cool-id', + atype: 3 + }] + }] + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_rubiconproject.com']).to.equal('some-cool-id'); + }); }); describe('Config user.id support', function () { @@ -1563,6 +1818,126 @@ describe('the rubicon adapter', function () { expect(data['tg_i.dfp_ad_unit_code']).to.equal('/a/b/c'); }); }); + + describe('client hints', function () { + let standardSuaObject; + beforeEach(function () { + standardSuaObject = { + source: 2, + platform: { + brand: 'macOS', + version: [ + '12', + '6', + '0' + ] + }, + browsers: [ + { + brand: 'Not.A/Brand', + version: [ + '8', + '0', + '0', + '0' + ] + }, + { + brand: 'Chromium', + version: [ + '114', + '0', + '5735', + '198' + ] + }, + { + brand: 'Google Chrome', + version: [ + '114', + '0', + '5735', + '198' + ] + } + ], + mobile: 0, + model: '', + bitness: '64', + architecture: 'x86' + } + }); + it('should send m_ch_* params if ortb2.device.sua object is there', function () { + let bidRequestSua = utils.deepClone(bidderRequest); + bidRequestSua.bids[0].ortb2 = { device: { sua: standardSuaObject } }; + + // How should fastlane query be constructed with default SUA + let expectedValues = { + m_ch_arch: 'x86', + m_ch_bitness: '64', + m_ch_ua: `"Not.A/Brand"|v="8","Chromium"|v="114","Google Chrome"|v="114"`, + m_ch_full_ver: `"Not.A/Brand"|v="8.0.0.0","Chromium"|v="114.0.5735.198","Google Chrome"|v="114.0.5735.198"`, + m_ch_mobile: '?0', + m_ch_platform: 'macOS', + m_ch_platform_ver: '12.6.0' + } + + // Build Fastlane call + let [request] = spec.buildRequests(bidRequestSua.bids, bidRequestSua); + let data = parseQuery(request.data); + + // Loop through expected values and if they do not match push an error + const errors = Object.entries(expectedValues).reduce((accum, [key, val]) => { + if (data[key] !== val) accum.push(`${key} - expect: ${val} - got: ${data[key]}`) + return accum; + }, []); + + // should be no errors + expect(errors).to.deep.equal([]); + }); + it('should not send invalid values for m_ch_*', function () { + let bidRequestSua = utils.deepClone(bidderRequest); + + // Alter input SUA object + // send model + standardSuaObject.model = 'Suface Duo'; + // send mobile = 1 + standardSuaObject.mobile = 1; + + // make browsers not an array + standardSuaObject.browsers = 'My Browser'; + + // make platform not have version + delete standardSuaObject.platform.version; + + // delete architecture + delete standardSuaObject.architecture; + + // add SUA to bid + bidRequestSua.bids[0].ortb2 = { device: { sua: standardSuaObject } }; + + // Build Fastlane request + let [request] = spec.buildRequests(bidRequestSua.bids, bidRequestSua); + let data = parseQuery(request.data); + + // should show new names + expect(data.m_ch_model).to.equal('Suface Duo'); + expect(data.m_ch_mobile).to.equal('?1'); + + // should still send platform + expect(data.m_ch_platform).to.equal('macOS'); + + // platform version not sent + expect(data).to.not.haveOwnProperty('m_ch_platform_ver'); + + // both ua and full_ver not sent because browsers not array + expect(data).to.not.haveOwnProperty('m_ch_ua'); + expect(data).to.not.haveOwnProperty('m_ch_full_ver'); + + // arch not sent + expect(data).to.not.haveOwnProperty('m_ch_arch'); + }); + }); }); if (FEATURES.VIDEO) { @@ -2344,6 +2719,15 @@ describe('the rubicon adapter', function () { const slotParams = spec.createSlotParams(bidderRequest.bids[0], bidderRequest); expect(slotParams.kw).to.equal('a,b,c'); }); + + it('should pass along o_ae param when fledge is enabled', () => { + const localBidRequest = Object.assign({}, bidderRequest.bids[0]); + localBidRequest.ortb2Imp.ext.ae = true; + + const slotParams = spec.createSlotParams(localBidRequest, bidderRequest); + + expect(slotParams['o_ae']).to.equal(1) + }); }); describe('classifiedAsVideo', function () { @@ -2407,6 +2791,18 @@ describe('the rubicon adapter', function () { expect(request.data.imp).to.have.nested.property('[0].native'); }); + it('should not break if position is set and no video MT', function () { + const bidReq = addNativeToBidRequest(bidderRequest); + delete bidReq.bids[0].mediaTypes.banner; + bidReq.bids[0].params = { + position: 'atf' + } + let [request] = spec.buildRequests(bidReq.bids, bidReq); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://prebid-server.rubiconproject.com/openrtb2/auction'); + expect(request.data.imp).to.have.nested.property('[0].native'); + }); + describe('that contains also a banner mediaType', function () { it('should send the banner to fastlane BUT NOT the native bid because missing params.video', function() { const bidReq = addNativeToBidRequest(bidderRequest); @@ -2458,6 +2854,35 @@ describe('the rubicon adapter', function () { expect(pbsRequest.data.imp).to.have.nested.property('[0].native'); expect(fastlanteRequest.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); }); + + it('should include multiformat data in the pbs request', () => { + const bidReq = addNativeToBidRequest(bidderRequest); + // add second mediaType + bidReq.bids[0].mediaTypes = { + ...bidReq.bids[0].mediaTypes, + banner: { + sizes: [[300, 250]] + } + }; + bidReq.bids[0].params.bidonmultiformat = true; + let [pbsRequest, fastlanteRequest] = spec.buildRequests(bidReq.bids, bidReq); + expect(pbsRequest.data.imp[0].ext.prebid.bidder.rubicon.formats).to.deep.equal(['native', 'banner']); + }); + + it('should include multiformat data in the fastlane request', () => { + const bidReq = addNativeToBidRequest(bidderRequest); + // add second mediaType + bidReq.bids[0].mediaTypes = { + ...bidReq.bids[0].mediaTypes, + banner: { + sizes: [[300, 250]] + } + }; + bidReq.bids[0].params.bidonmultiformat = true; + let [pbsRequest, fastlanteRequest] = spec.buildRequests(bidReq.bids, bidReq); + let formatsIncluded = fastlanteRequest.data.indexOf('formats=native%2Cbanner') !== -1; + expect(formatsIncluded).to.equal(true); + }); }); describe('with bidonmultiformat === false', () => { it('should send only banner request because there\'s no params.video', () => { @@ -3023,6 +3448,43 @@ describe('the rubicon adapter', function () { expect(bids).to.be.lengthOf(0); }); + it('Should support recieving an auctionConfig and pass it along to Prebid', function () { + let response = { + 'status': 'ok', + 'account_id': 14062, + 'site_id': 70608, + 'zone_id': 530022, + 'size_id': 15, + 'alt_size_ids': [ + 43 + ], + 'tracking': '', + 'inventory': {}, + 'ads': [{ + 'status': 'ok', + 'cpm': 0, + 'size_id': 15 + }], + 'component_auction_config': [{ + 'random': 'value', + 'bidId': '5432' + }, + { + 'random': 'string', + 'bidId': '6789' + }] + }; + + let {bids, fledgeAuctionConfigs} = spec.interpretResponse({body: response}, { + bidRequest: bidderRequest.bids[0] + }); + + expect(bids).to.be.lengthOf(1); + expect(fledgeAuctionConfigs[0].bidId).to.equal('5432'); + expect(fledgeAuctionConfigs[0].config.random).to.equal('value'); + expect(fledgeAuctionConfigs[1].bidId).to.equal('6789'); + }); + it('should handle an error', function () { let response = { 'status': 'ok', @@ -3653,6 +4115,24 @@ describe('the rubicon adapter', function () { type: 'iframe', url: `${emilyUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }); }); + + it('should pass gpp params when gppConsent is present', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, {}, undefined, { + gppString: 'foo', + applicableSections: [2] + })).to.deep.equal({ + type: 'iframe', url: `${emilyUrl}?gpp=foo&gpp_sid=2` + }); + }); + + it('should pass multiple sid\'s when multiple are present', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, {}, undefined, { + gppString: 'foo', + applicableSections: [2, 5] + })).to.deep.equal({ + type: 'iframe', url: `${emilyUrl}?gpp=foo&gpp_sid=2,5` + }); + }); }); describe('get price granularity', function () { diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 4989dcb1098..eb0b45dda11 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import * as sinon from 'sinon'; import { sharethroughAdapterSpec, sharethroughInternal } from 'modules/sharethroughBidAdapter.js'; +import * as sinon from 'sinon'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config'; import * as utils from 'src/utils'; @@ -73,7 +73,10 @@ describe('sharethrough adapter spec', function () { bidder: 'sharethrough', bidId: 'bidId1', transactionId: 'transactionId1', - sizes: [[300, 250], [300, 600]], + sizes: [ + [300, 250], + [300, 600], + ], params: { pkey: 'aaaa1111', bcat: ['cat1', 'cat2'], @@ -95,104 +98,104 @@ describe('sharethrough adapter spec', function () { }, userIdAsEids: [ { - 'source': 'pubcid.org', - 'uids': [ + source: 'pubcid.org', + uids: [ { - 'atype': 1, - 'id': 'fake-pubcid' + atype: 1, + id: 'fake-pubcid', }, - ] + ], }, { - 'source': 'liveramp.com', - 'uids': [ + source: 'liveramp.com', + uids: [ { - 'atype': 1, - 'id': 'fake-identity-link' - } - ] + atype: 1, + id: 'fake-identity-link', + }, + ], }, { - 'source': 'id5-sync.com', - 'uids': [ + source: 'id5-sync.com', + uids: [ { - 'atype': 1, - 'id': 'fake-id5id' - } - ] + atype: 1, + id: 'fake-id5id', + }, + ], }, { - 'source': 'adserver.org', - 'uids': [ + source: 'adserver.org', + uids: [ { - 'atype': 1, - 'id': 'fake-tdid' - } - ] + atype: 1, + id: 'fake-tdid', + }, + ], }, { - 'source': 'criteo.com', - 'uids': [ + source: 'criteo.com', + uids: [ { - 'atype': 1, - 'id': 'fake-criteo' - } - ] + atype: 1, + id: 'fake-criteo', + }, + ], }, { - 'source': 'britepool.com', - 'uids': [ + source: 'britepool.com', + uids: [ { - 'atype': 1, - 'id': 'fake-britepool' - } - ] + atype: 1, + id: 'fake-britepool', + }, + ], }, { - 'source': 'liveintent.com', - 'uids': [ + source: 'liveintent.com', + uids: [ { - 'atype': 1, - 'id': 'fake-lipbid' - } - ] + atype: 1, + id: 'fake-lipbid', + }, + ], }, { - 'source': 'intentiq.com', - 'uids': [ + source: 'intentiq.com', + uids: [ { - 'atype': 1, - 'id': 'fake-intentiq' - } - ] + atype: 1, + id: 'fake-intentiq', + }, + ], }, { - 'source': 'crwdcntrl.net', - 'uids': [ + source: 'crwdcntrl.net', + uids: [ { - 'atype': 1, - 'id': 'fake-lotame' - } - ] + atype: 1, + id: 'fake-lotame', + }, + ], }, { - 'source': 'parrable.com', - 'uids': [ + source: 'parrable.com', + uids: [ { - 'atype': 1, - 'id': 'fake-parrable' - } - ] + atype: 1, + id: 'fake-parrable', + }, + ], }, { - 'source': 'netid.de', - 'uids': [ + source: 'netid.de', + uids: [ { - 'atype': 1, - 'id': 'fake-netid' - } - ] - } + atype: 1, + id: 'fake-netid', + }, + ], + }, ], crumbs: { pubcid: 'fake-pubcid-in-crumbs-obj', @@ -250,10 +253,10 @@ describe('sharethrough adapter spec', function () { }, ortb2: { source: { - tid: 'auction-id' - } + tid: 'auction-id', + }, }, - timeout: 242 + timeout: 242, }; }); @@ -312,6 +315,12 @@ describe('sharethrough adapter spec', function () { expect(eid.uids[0].atype).to.be.ok; } + // expect(openRtbReq.regs.gpp).to.equal(bidderRequest.gppConsent.gppString); + // expect(openRtbReq.regs.gpp_sid).to.equal(bidderRequest.gppConsent.applicableSections); + + // expect(openRtbReq.regs.ext.gpp).to.equal(bidderRequest.ortb2.regs.gpp); + // expect(openRtbReq.regs.ext.gpp_sid).to.equal(bidderRequest.ortb2.regs.gpp_sid); + expect(openRtbReq.device.ua).to.equal(navigator.userAgent); expect(openRtbReq.regs.coppa).to.equal(1); @@ -395,6 +404,25 @@ describe('sharethrough adapter spec', function () { expect(openRtbReq.regs.coppa).to.equal(0); }); }); + + describe('gpp', () => { + it('should properly attach GPP information to the request when applicable', () => { + bidderRequest.gppConsent = { + gppString: 'some-gpp-string', + applicableSections: [3, 5], + }; + + const openRtbReq = spec.buildRequests(bidRequests, bidderRequest)[0].data; + expect(openRtbReq.regs.gpp).to.equal(bidderRequest.gppConsent.gppString); + expect(openRtbReq.regs.gpp_sid).to.equal(bidderRequest.gppConsent.applicableSections); + }); + + it('should populate request accordingly when gpp explicitly does not apply', function () { + const openRtbReq = spec.buildRequests(bidRequests, {})[0].data; + + expect(openRtbReq.regs.gpp).to.be.undefined; + }); + }); }); describe('transaction id at the impression level', () => { @@ -455,7 +483,10 @@ describe('sharethrough adapter spec', function () { const bannerImp = builtRequest.data.imp[0].banner; expect(bannerImp.pos).to.equal(1); expect(bannerImp.topframe).to.equal(1); - expect(bannerImp.format).to.deep.equal([{ w: 300, h: 250 }, { w: 300, h: 600 }]); + expect(bannerImp.format).to.deep.equal([ + { w: 300, h: 250 }, + { w: 300, h: 600 }, + ]); }); it('should default to pos 0 if not provided', () => { @@ -585,10 +616,14 @@ describe('sharethrough adapter spec', function () { }, bcat: ['IAB1', 'IAB2-1'], badv: ['domain1.com', 'domain2.com'], + regs: { + gpp: 'gpp_string', + gpp_sid: [7], + }, }; it('should include first party data in open rtb request, site section', () => { - const openRtbReq = spec.buildRequests(bidRequests, {...bidderRequest, ortb2: firstPartyData})[0].data; + const openRtbReq = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2: firstPartyData })[0].data; expect(openRtbReq.site.name).to.equal(firstPartyData.site.name); expect(openRtbReq.site.keywords).to.equal(firstPartyData.site.keywords); @@ -612,6 +647,13 @@ describe('sharethrough adapter spec', function () { expect(openRtbReq.bcat).to.deep.equal(firstPartyData.bcat); expect(openRtbReq.badv).to.deep.equal(firstPartyData.badv); }); + + it('should include first party data in open rtb request, regulation section', () => { + const openRtbReq = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2: firstPartyData })[0].data; + + expect(openRtbReq.regs.ext.gpp).to.equal(firstPartyData.regs.gpp); + expect(openRtbReq.regs.ext.gpp_sid).to.equal(firstPartyData.regs.gpp_sid); + }); }); }); @@ -624,26 +666,31 @@ describe('sharethrough adapter spec', function () { request = spec.buildRequests(bidRequests, bidderRequest)[0]; response = { body: { - seatbid: [{ - bid: [{ - id: '123', - impid: 'bidId1', - w: 300, - h: 250, - price: 42, - crid: 'creative', - dealid: 'deal', - adomain: ['domain.com'], - adm: 'markup', - }, { - id: '456', - impid: 'bidId2', - w: 640, - h: 480, - price: 42, - adm: 'vastTag', - }], - }], + seatbid: [ + { + bid: [ + { + id: '123', + impid: 'bidId1', + w: 300, + h: 250, + price: 42, + crid: 'creative', + dealid: 'deal', + adomain: ['domain.com'], + adm: 'markup', + }, + { + id: '456', + impid: 'bidId2', + w: 640, + h: 480, + price: 42, + adm: 'vastTag', + }, + ], + }, + ], }, }; }); @@ -673,16 +720,20 @@ describe('sharethrough adapter spec', function () { request = spec.buildRequests(bidRequests, bidderRequest)[1]; response = { body: { - seatbid: [{ - bid: [{ - id: '456', - impid: 'bidId2', - w: 640, - h: 480, - price: 42, - adm: 'vastTag', - }], - }], + seatbid: [ + { + bid: [ + { + id: '456', + impid: 'bidId2', + w: 640, + h: 480, + price: 42, + adm: 'vastTag', + }, + ], + }, + ], }, }; }); @@ -712,24 +763,28 @@ describe('sharethrough adapter spec', function () { request = spec.buildRequests(bidRequests, bidderRequest)[0]; response = { body: { - seatbid: [{ - bid: [{ - id: '123', - impid: 'bidId1', - w: 300, - h: 250, - price: 42, - crid: 'creative', - dealid: 'deal', - adomain: ['domain.com'], - adm: 'markup', - }], - }], + seatbid: [ + { + bid: [ + { + id: '123', + impid: 'bidId1', + w: 300, + h: 250, + price: 42, + crid: 'creative', + dealid: 'deal', + adomain: ['domain.com'], + adm: 'markup', + }, + ], + }, + ], }, }; }); - it('should have null optional fields when the response\'s optional seatbid[].bid[].ext field is empty', () => { + it("should have null optional fields when the response's optional seatbid[].bid[].ext field is empty", () => { const bid = spec.interpretResponse(response, request)[0]; expect(bid.meta.networkId).to.be.null; @@ -747,7 +802,7 @@ describe('sharethrough adapter spec', function () { expect(bid.meta.mediaType).to.be.null; }); - it('should have populated fields when the response\'s optional seatbid[].bid[].ext fields are filled', () => { + it("should have populated fields when the response's optional seatbid[].bid[].ext fields are filled", () => { response.body.seatbid[0].bid[0].ext = { networkId: 'my network id', networkName: 'my network name', @@ -792,8 +847,8 @@ describe('sharethrough adapter spec', function () { expect(syncArray).to.deep.equal([ { type: 'image', url: 'cookieUrl1' }, { type: 'image', url: 'cookieUrl2' }, - { type: 'image', url: 'cookieUrl3' }], - ); + { type: 'image', url: 'cookieUrl3' }, + ]); }); it('returns an empty array if serverResponses is empty', function () { diff --git a/test/spec/modules/smaatoBidAdapter_spec.js b/test/spec/modules/smaatoBidAdapter_spec.js index 61a877f329d..185dee2430f 100644 --- a/test/spec/modules/smaatoBidAdapter_spec.js +++ b/test/spec/modules/smaatoBidAdapter_spec.js @@ -1095,17 +1095,15 @@ describe('smaatoBidAdapterTest', () => { criteoId: '123456', tdid: '89145' }, - userIdAsEids: createEidsArray({ - criteoId: '123456', - tdid: '89145' - }) + userIdAsEids: [ + {id: 1}, {id: 2} + ] }; const reqs = spec.buildRequests([userIdBidRequest], defaultBidderRequest); const req = extractPayloadOfFirstAndOnlyRequest(reqs); - expect(req.user.ext.eids).to.exist; - expect(req.user.ext.eids).to.have.length(2); + expect(req.user.ext.eids).to.eql(userIdBidRequest.userIdAsEids); }); }); diff --git a/test/spec/modules/smartyadsBidAdapter_spec.js b/test/spec/modules/smartyadsBidAdapter_spec.js index 3474753c838..458ccc37759 100644 --- a/test/spec/modules/smartyadsBidAdapter_spec.js +++ b/test/spec/modules/smartyadsBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from '../../../modules/smartyadsBidAdapter.js'; import { config } from '../../../src/config.js'; +import {server} from '../../mocks/xhr'; describe('SmartyadsAdapter', function () { let bid = { @@ -14,6 +15,21 @@ describe('SmartyadsAdapter', function () { } }; + let bidResponse = { + width: 300, + height: 250, + mediaType: 'banner', + ad: `test mode`, + requestId: '23fhj33i987f', + cpm: 0.1, + ttl: 120, + creativeId: '123', + netRevenue: true, + currency: 'USD', + dealId: 'HASH', + sid: 1234 + }; + describe('isBidRequestValid', function () { it('Should return true if there are bidId, params and sourceid parameters present', function () { expect(spec.isBidRequestValid(bid)).to.be.true; @@ -36,7 +52,11 @@ describe('SmartyadsAdapter', function () { expect(serverRequest.method).to.equal('POST'); }); it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js'); + expect(serverRequest.url).to.be.oneOf([ + 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + 'https://n2.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + 'https://n6.smartyads.com/?c=o&m=prebid&secret_key=prebid_js' + ]); }); it('Returns valid data if array of bids is valid', function () { let data = serverRequest.data; @@ -243,7 +263,7 @@ describe('SmartyadsAdapter', function () { }); }); describe('getUserSyncs', function () { - const syncUrl = 'https://as.ck-ie.com/prebidjs?p=7c47322e527cf8bdeb7facc1bb03387a&gdpr=0&gdpr_consent=&type=iframe&us_privacy='; + const syncUrl = 'https://as.ck-ie.com/prebidjs?p=7c47322e527cf8bdeb7facc1bb03387a&gdpr=0&gdpr_consent=&type=iframe&us_privacy=&gpp='; const syncOptions = { iframeEnabled: true }; @@ -257,4 +277,79 @@ describe('SmartyadsAdapter', function () { ]); }); }); + + describe('onBidWon', function () { + it('should exists', function () { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bid won notice', function () { + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'winTest': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onBidWon(bid); + expect(server.requests.length).to.equal(1); + }); + }); + + describe('onTimeout', function () { + it('should exists', function () { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bid timeout notice', function () { + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'bidTimeout': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onTimeout(bid); + expect(server.requests.length).to.equal(1); + }); + }); + + describe('onBidderError', function () { + it('should exists', function () { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bidder error notice', function () { + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'bidderError': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onBidderError(bid); + expect(server.requests.length).to.equal(1); + }); + }); }); diff --git a/test/spec/modules/snigelBidAdapter_spec.js b/test/spec/modules/snigelBidAdapter_spec.js index 3fc09493f03..3ba84228872 100644 --- a/test/spec/modules/snigelBidAdapter_spec.js +++ b/test/spec/modules/snigelBidAdapter_spec.js @@ -20,8 +20,10 @@ const makeBidRequest = function (overrides) { }; const BASE_BIDDER_REQUEST = { + auctionId: 'test', bidderRequestId: 'test', refererInfo: { + page: 'https://localhost', canonicalUrl: 'https://localhost', }, }; @@ -53,8 +55,8 @@ describe('snigelBidAdapter', function () { it('should build a single request for every impression and its placement', function () { const bidderRequest = Object.assign({}, BASE_BIDDER_REQUEST); const bidRequests = [ - makeBidRequest({bidId: 'a', params: {placement: 'top_leaderboard'}}), - makeBidRequest({bidId: 'b', params: {placement: 'bottom_leaderboard'}}), + makeBidRequest({bidId: 'a', adUnitCode: 'au_a', params: {placement: 'top_leaderboard'}}), + makeBidRequest({bidId: 'b', adUnitCode: 'au_b', params: {placement: 'bottom_leaderboard'}}), ]; const request = spec.buildRequests(bidRequests, bidderRequest); @@ -70,9 +72,9 @@ describe('snigelBidAdapter', function () { expect(data).to.have.property('page').and.to.equal('https://localhost'); expect(data).to.have.property('placements'); expect(data.placements.length).to.equal(2); - expect(data.placements[0].uuid).to.equal('a'); + expect(data.placements[0].id).to.equal('au_a'); expect(data.placements[0].name).to.equal('top_leaderboard'); - expect(data.placements[1].uuid).to.equal('b'); + expect(data.placements[1].id).to.equal('au_b'); expect(data.placements[1].name).to.equal('bottom_leaderboard'); }); @@ -127,6 +129,56 @@ describe('snigelBidAdapter', function () { const data = JSON.parse(request.data); expect(data).to.have.property('coppa').and.to.equal(true); }); + + it('should forward refresh information', function () { + const bidderRequest = Object.assign({}, BASE_BIDDER_REQUEST); + const topLeaderboard = makeBidRequest({adUnitCode: 'top_leaderboard'}); + const bottomLeaderboard = makeBidRequest({adUnitCode: 'bottom_leaderboard'}); + const sidebar = makeBidRequest({adUnitCode: 'sidebar'}); + + // first auction, no refresh + let request = spec.buildRequests([topLeaderboard, bottomLeaderboard], bidderRequest); + expect(request).to.have.property('data'); + let data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(2); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.be.undefined; + expect(data.placements[1].id).to.equal('bottom_leaderboard'); + expect(data.placements[1].refresh).to.be.undefined; + + // second auction for top leaderboard, was refreshed + request = spec.buildRequests([topLeaderboard, sidebar], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(2); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.not.be.undefined; + expect(data.placements[0].refresh.count).to.equal(1); + expect(data.placements[0].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[1].id).to.equal('sidebar'); + expect(data.placements[1].refresh).to.be.undefined; + + // third auction, all units refreshed at some point + request = spec.buildRequests([topLeaderboard, bottomLeaderboard, sidebar], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(3); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.not.be.undefined; + expect(data.placements[0].refresh.count).to.equal(2); + expect(data.placements[0].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[1].id).to.equal('bottom_leaderboard'); + expect(data.placements[1].refresh).to.not.be.undefined; + expect(data.placements[1].refresh.count).to.equal(1); + expect(data.placements[1].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[2].id).to.equal('sidebar'); + expect(data.placements[2].refresh).to.not.be.undefined; + expect(data.placements[2].refresh.count).to.equal(1); + expect(data.placements[2].refresh.time).to.be.greaterThanOrEqual(0); + }); }); describe('interpretResponse', function () { @@ -146,7 +198,7 @@ describe('snigelBidAdapter', function () { cur: 'USD', bids: [ { - uuid: BASE_BID_REQUEST.bidId, + id: BASE_BID_REQUEST.adUnitCode, price: 0.0575, ad: '

Test Ad

', width: 728, @@ -160,7 +212,7 @@ describe('snigelBidAdapter', function () { }, }; - const bids = spec.interpretResponse(serverResponse, {}); + const bids = spec.interpretResponse(serverResponse, {bidderRequest: {bids: [BASE_BID_REQUEST]}}); expect(bids.length).to.equal(1); const bid = bids[0]; expect(isValid(BASE_BID_REQUEST.adUnitCode, bid)).to.be.true; diff --git a/test/spec/modules/sonobiAnalyticsAdapter_spec.js b/test/spec/modules/sonobiAnalyticsAdapter_spec.js index 76ff88836d4..ed8ccd22eea 100644 --- a/test/spec/modules/sonobiAnalyticsAdapter_spec.js +++ b/test/spec/modules/sonobiAnalyticsAdapter_spec.js @@ -1,4 +1,4 @@ -import sonobiAnalytics from 'modules/sonobiAnalyticsAdapter.js'; +import sonobiAnalytics, {DEFAULT_EVENT_URL} from 'modules/sonobiAnalyticsAdapter.js'; import {expect} from 'chai'; import {server} from 'test/mocks/xhr.js'; let events = require('src/events'); @@ -76,8 +76,8 @@ describe('Sonobi Prebid Analytic', function () { events.emit(constants.EVENTS.AUCTION_END, {auctionId: '13', bidsReceived: [bid]}); clock.tick(5000); - expect(server.requests).to.have.length(1); - expect(JSON.parse(server.requests[0].requestBody)).to.have.length(3) + const req = server.requests.find(req => req.url.indexOf(DEFAULT_EVENT_URL) !== -1); + expect(JSON.parse(req.requestBody)).to.have.length(3) done(); }); }); diff --git a/test/spec/modules/sonobiBidAdapter_spec.js b/test/spec/modules/sonobiBidAdapter_spec.js index de8d0a5bda7..164aa06d9b7 100644 --- a/test/spec/modules/sonobiBidAdapter_spec.js +++ b/test/spec/modules/sonobiBidAdapter_spec.js @@ -1,9 +1,9 @@ -import { expect } from 'chai' -import { spec, _getPlatform } from 'modules/sonobiBidAdapter.js' -import { newBidder } from 'src/adapters/bidderFactory.js' -import { userSync } from '../../../src/userSync.js'; -import { config } from 'src/config.js'; -import * as utils from '../../../src/utils.js'; +import {expect} from 'chai'; +import {_getPlatform, spec} from 'modules/sonobiBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import {userSync} from '../../../src/userSync.js'; +import {config} from 'src/config.js'; +import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; describe('SonobiBidAdapter', function () { const adapter = newBidder(spec) @@ -248,13 +248,13 @@ describe('SonobiBidAdapter', function () { let sandbox; beforeEach(function () { sinon.stub(userSync, 'canBidderRegisterSync'); - sinon.stub(utils, 'getGptSlotInfoForAdUnitCode') + sinon.stub(gptUtils, 'getGptSlotInfoForAdUnitCode') .onFirstCall().returns({ gptSlot: '/123123/gpt_publisher/adunit-code-3', divId: 'adunit-code-3-div-id' }); sandbox = sinon.createSandbox(); }); afterEach(function () { userSync.canBidderRegisterSync.restore(); - utils.getGptSlotInfoForAdUnitCode.restore(); + gptUtils.getGptSlotInfoForAdUnitCode.restore(); sandbox.restore(); }); let bidRequest = [{ @@ -295,7 +295,10 @@ describe('SonobiBidAdapter', function () { mediaTypes: { video: { playerSize: [640, 480], - context: 'outstream' + context: 'outstream', + playbackmethod: [1, 2, 3], + plcmt: 3, + placement: 2 } } }, @@ -339,7 +342,7 @@ describe('SonobiBidAdapter', function () { }]; let keyMakerData = { - '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d|640x480|f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,', + '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d|640x480|f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,pm=1:2:3,p=2,pl=3,', '30b31c1838de1d': '1a2b3c4d5e6f1a2b3c4e|300x250,300x600|f=0.42,gpid=/123123/gpt_publisher/adunit-code-3,c=d,', '/7780971/sparks_prebid_LB|30b31c1838de1e': '300x250,300x600|gpid=/7780971/sparks_prebid_LB,c=d,', }; diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index c84013d1963..032f959e559 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -64,6 +64,29 @@ describe('sovrnBidAdapter', function() { expect(spec.isBidRequestValid(bidRequest)).to.equal(false) }) + + it('should return true when minduration is not passed', function() { + const width = 300 + const height = 250 + const mimes = ['video/mp4', 'application/javascript'] + const protocols = [2, 5] + const maxduration = 60 + const startdelay = 0 + const videoBidRequest = { + ...baseBidRequest, + mediaTypes: { + video: { + mimes, + protocols, + playerSize: [[width, height], [360, 240]], + maxduration, + startdelay + } + } + } + + expect(spec.isBidRequestValid(videoBidRequest)).to.equal(true) + }) }) describe('buildRequests', function () { @@ -183,6 +206,40 @@ describe('sovrnBidAdapter', function() { expect(payload.tmax).to.equal(3000) }) + it('forwards auction level tid', function() { + const bidderRequest = { + ...baseBidderRequest, + ortb2: { + source: { + tid: '1d1a030790a475' + } + }, + bids: [baseBidRequest] + } + + const payload = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) + expect(payload.source?.tid).to.equal('1d1a030790a475') + }) + + it('forwards impression level tid', function() { + const bidRequest = { + ...baseBidRequest, + ortb2Imp: { + ext: { + tid: '1a2c032473f4983' + } + }, + } + + const bidderRequest = { + ...baseBidderRequest, + bids: [bidRequest] + } + + const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest).data) + expect(payload.imp[0]?.ext?.tid).to.equal('1a2c032473f4983') + }) + it('includes the ad unit code in the request', function() { const impression = payload.imp[0] expect(impression.adunitcode).to.equal('adunit-code') @@ -817,7 +874,7 @@ describe('sovrnBidAdapter', function() { url: `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&informer=13487408`, } - const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, '') + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, '', null) expect(returnStatement[0]).to.deep.equal(expectedReturnStatement) }) @@ -829,7 +886,22 @@ describe('sovrnBidAdapter', function() { url: `https://ap.lijit.com/beacon?us_privacy=${uspString}&informer=13487408`, } - const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, null, uspString) + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, null, uspString, null) + + expect(returnStatement[0]).to.deep.equal(expectedReturnStatement) + }) + + it('should include gpp consent string if present', function() { + const gppConsent = { + applicableSections: [1, 2], + gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN' + } + const expectedReturnStatement = { + type: 'iframe', + url: `https://ap.lijit.com/beacon?gpp=${gppConsent.gppString}&gpp_sid=${gppConsent.applicableSections}&informer=13487408`, + } + + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, null, '', gppConsent) expect(returnStatement[0]).to.deep.equal(expectedReturnStatement) }) @@ -840,12 +912,17 @@ describe('sovrnBidAdapter', function() { consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' } const uspString = '1NYN' + const gppConsent = { + applicableSections: [1, 2], + gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN' + } + const expectedReturnStatement = { type: 'iframe', - url: `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&us_privacy=${uspString}&informer=13487408`, + url: `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&us_privacy=${uspString}&gpp=${gppConsent.gppString}&gpp_sid=${gppConsent.applicableSections}&informer=13487408`, } - const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, uspString) + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, uspString, gppConsent) expect(returnStatement[0]).to.deep.equal(expectedReturnStatement) }) diff --git a/test/spec/modules/sparteoBidAdapter_spec.js b/test/spec/modules/sparteoBidAdapter_spec.js new file mode 100644 index 00000000000..e82f23a1d4e --- /dev/null +++ b/test/spec/modules/sparteoBidAdapter_spec.js @@ -0,0 +1,408 @@ +import {expect} from 'chai'; +import { deepClone, mergeDeep } from 'src/utils'; +import {spec as adapter} from 'modules/sparteoBidAdapter'; + +const CURRENCY = 'EUR'; +const TTL = 60; +const HTTP_METHOD = 'POST'; +const REQUEST_URL = 'https://bid.sparteo.com/auction'; + +const VALID_BID_BANNER = { + bidder: 'sparteo', + bidId: '1a2b3c4d', + adUnitCode: 'id-1234', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + }, + mediaTypes: { + banner: { + sizes: [ + [1, 1] + ] + } + } +}; + +const VALID_BID_VIDEO = { + bidder: 'sparteo', + bidId: '5e6f7g8h', + adUnitCode: 'id-5678', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + }, + mediaTypes: { + video: { + playerSize: [640, 360], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + api: [1, 2], + mimes: ['video/mp4'], + skip: 1, + startdelay: 0, + placement: 1, + linearity: 1, + minduration: 5, + maxduration: 30, + context: 'instream' + } + }, + ortb2Imp: { + ext: { + pbadslot: 'video' + } + } +}; + +const VALID_REQUEST_BANNER = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '1a2b3c4d', + 'banner': { + 'format': [{ + 'h': 1, + 'w': 1 + }], + 'topframe': 0 + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const VALID_REQUEST_VIDEO = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '5e6f7g8h', + 'video': { + 'w': 640, + 'h': 360, + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 2], + 'mimes': ['video/mp4'], + 'skip': 1, + 'startdelay': 0, + 'placement': 1, + 'linearity': 1, + 'minduration': 5, + 'maxduration': 30, + }, + 'ext': { + 'pbadslot': 'video' + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const VALID_REQUEST = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '1a2b3c4d', + 'banner': { + 'format': [{ + 'h': 1, + 'w': 1 + }], + 'topframe': 0 + } + }, { + 'id': '5e6f7g8h', + 'video': { + 'w': 640, + 'h': 360, + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 2], + 'mimes': ['video/mp4'], + 'skip': 1, + 'startdelay': 0, + 'placement': 1, + 'linearity': 1, + 'minduration': 5, + 'maxduration': 30, + }, + 'ext': { + 'pbadslot': 'video' + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const BIDDER_REQUEST = { + bids: [VALID_BID_BANNER, VALID_BID_VIDEO] +} + +const BIDDER_REQUEST_BANNER = { + bids: [VALID_BID_BANNER] +} + +const BIDDER_REQUEST_VIDEO = { + bids: [VALID_BID_VIDEO] +} + +describe('SparteoAdapter', function () { + describe('isBidRequestValid', function () { + describe('Check method return', function () { + it('should return true', function () { + expect(adapter.isBidRequestValid(VALID_BID_BANNER)).to.equal(true); + expect(adapter.isBidRequestValid(VALID_BID_VIDEO)).to.equal(true); + }); + + it('should return false because the networkId is missing', function () { + let wrongBid = deepClone(VALID_BID_BANNER); + delete wrongBid.params.networkId; + + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + + it('should return false because the banner size is missing', function () { + let wrongBid = deepClone(VALID_BID_BANNER); + + wrongBid.mediaTypes.banner.sizes = '123456'; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + + delete wrongBid.mediaTypes.banner.sizes; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + + it('should return false because the video player size paramater is missing', function () { + let wrongBid = deepClone(VALID_BID_VIDEO); + + wrongBid.mediaTypes.video.playerSize = '123456'; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + + delete wrongBid.mediaTypes.video.playerSize; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + }); + }); + + describe('buildRequests', function () { + describe('Check method return', function () { + if (FEATURES.VIDEO) { + it('should return the right formatted requests', function() { + const request = adapter.buildRequests([VALID_BID_BANNER, VALID_BID_VIDEO], BIDDER_REQUEST); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST); + }); + } + + it('should return the right formatted banner requests', function() { + const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST_BANNER); + }); + + if (FEATURES.VIDEO) { + it('should return the right formatted video requests', function() { + const request = adapter.buildRequests([VALID_BID_VIDEO], BIDDER_REQUEST_VIDEO); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST_VIDEO); + }); + } + + it('should return the right formatted request with endpoint test', function() { + let endpoint = 'https://bid-test.sparteo.com/auction'; + + let bids = mergeDeep(deepClone([VALID_BID_BANNER, VALID_BID_VIDEO]), { + params: { + endpoint: endpoint + } + }); + + let requests = mergeDeep(deepClone(VALID_REQUEST)); + + const request = adapter.buildRequests(bids, BIDDER_REQUEST); + requests.url = endpoint; + delete request.data.id; + + expect(requests).to.deep.equal(requests); + }); + }); + }); + + describe('interpretResponse', function() { + describe('Check method return', function () { + it('should return the right formatted response', function() { + let response = { + body: { + 'id': '63f4d300-6896-4bdc-8561-0932f73148b1', + 'cur': 'EUR', + 'seatbid': [ + { + 'seat': 'sparteo', + 'group': 0, + 'bid': [ + { + 'id': 'cdbb6982-a269-40c7-84e5-04797f11d87a', + 'impid': '1a2b3c4d', + 'price': 4.5, + 'ext': { + 'prebid': { + 'type': 'banner' + } + }, + 'adm': 'script', + 'crid': 'crid', + 'w': 1, + 'h': 1, + 'nurl': 'https://t.bidder.sparteo.com/img' + } + ] + } + ] + } + }; + + if (FEATURES.VIDEO) { + response.body.seatbid[0].bid.push({ + 'id': 'cdbb6982-a269-40c7-84e5-04797f11d87b', + 'impid': '5e6f7g8h', + 'price': 5, + 'ext': { + 'prebid': { + 'type': 'video' + } + }, + 'adm': 'tag', + 'crid': 'crid', + 'w': 640, + 'h': 480, + 'nurl': 'https://t.bidder.sparteo.com/img' + }); + } + + let formattedReponse = [ + { + requestId: '1a2b3c4d', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87a', + cpm: 4.5, + width: 1, + height: 1, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'banner', + meta: {}, + ad: 'script
' + } + ]; + + if (FEATURES.VIDEO) { + formattedReponse.push({ + requestId: '5e6f7g8h', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87b', + cpm: 5, + width: 640, + height: 480, + playerWidth: 640, + playerHeight: 360, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + vastUrl: 'https://t.bidder.sparteo.com/img', + vastXml: 'tag' + }); + } + + if (FEATURES.VIDEO) { + const request = adapter.buildRequests([VALID_BID_BANNER, VALID_BID_VIDEO], BIDDER_REQUEST); + expect(adapter.interpretResponse(response, request)).to.deep.equal(formattedReponse); + } else { + const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + expect(adapter.interpretResponse(response, request)).to.deep.equal(formattedReponse); + } + }); + }); + }); + + describe('onBidWon', function() { + describe('Check methods succeed', function () { + it('should not throw error', function() { + let bids = [ + { + requestId: '1a2b3c4d', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87a', + cpm: 4.5, + width: 1, + height: 1, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'banner', + meta: {}, + ad: 'script
', + nurl: [ + 'win.domain.com' + ] + }, + { + requestId: '2570', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87b', + id: 'id-5678', + cpm: 5, + width: 640, + height: 480, + creativeId: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + vastXml: 'vast xml', + nurl: [ + 'win.domain2.com' + ] + } + ]; + + bids.forEach(function(bid) { + expect(adapter.onBidWon.bind(adapter, bid)).to.not.throw(); + }); + }); + }); + }); +}); diff --git a/test/spec/modules/ssmasBidAdapter_spec.js b/test/spec/modules/ssmasBidAdapter_spec.js new file mode 100644 index 00000000000..26c6f60da4b --- /dev/null +++ b/test/spec/modules/ssmasBidAdapter_spec.js @@ -0,0 +1,244 @@ +import { expect } from 'chai'; +import { spec, SSMAS_CODE, SSMAS_ENDPOINT, SSMAS_REQUEST_METHOD } from 'modules/ssmasBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import * as utils from 'src/utils.js'; + +describe('ssmasBidAdapter', function () { + const bid = { + bidder: SSMAS_CODE, + adUnitCode: 'adunit-code', + sizes: [[300, 250]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + params: { + placementId: '1' + } + }; + + const bidderRequest = { + 'bidderCode': SSMAS_CODE, + 'auctionId': 'd912faa2-174f-4636-b755-7396a0a964d8', + 'bidderRequestId': '109db5a5f5c6788', + 'bids': [ + bid + ], + 'auctionStart': 1684799653734, + 'timeout': 20000, + 'metrics': {}, + 'ortb2': { + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/noadserver/basic_noadserver.html', + 'ref': 'http://localhost:9999/integrationExamples/noadserver/' + }, + 'device': { + 'w': 1536, + 'h': 711, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0', + 'language': 'es' + } + }, + 'start': 1684799653737 + }; + + describe('Build Requests', () => { + it('Check bid request', function () { + const request = spec.buildRequests([bid], bidderRequest); + expect(request[0].method).to.equal(SSMAS_REQUEST_METHOD); + expect(request[0].url).to.equal(SSMAS_ENDPOINT); + }); + }); + + describe('register adapter functions', () => { + const adapter = newBidder(spec); + it('is registered', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('validate bid request building', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('test bad bid request', function () { + // empty bid + expect(spec.isBidRequestValid({bidId: '', params: {}})).to.be.false; + + // empty bidId + bid.bidId = ''; + expect(spec.isBidRequestValid(bid)).to.be.false; + + // empty placementId + bid.bidId = '1231'; + bid.params.placementId = ''; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('check bid request bidder is Sem Seo & Mas', function() { + const invalidBid = { + ...bid, bidder: 'invalidBidder' + }; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('interpretResponse', function () { + let bidOrtbResponse = { + 'id': 'aa02e2fe-56d9-4713-88f9-d8672ceae8ab', + 'seatbid': [ + { + 'bid': [ + { + 'id': '0001', + 'impid': '3919400af0b73e8', + 'price': 7.01, + 'adid': null, + 'nurl': null, + 'adm': '', + 'adomain': [ + 'ssmas.com' + ], + 'iurl': null, + 'cid': null, + 'crid': '3547894', + 'attr': [], + 'api': 0, + 'protocol': 0, + 'dealid': null, + 'h': 600, + 'w': 300, + 'cat': null, + 'ext': null, + 'builder': { + 'id': '0001', + 'adid': null, + 'impid': '3919400af0b73e8', + 'adomainList': [ + 'ssmas.com' + ], + 'attrList': [] + }, + 'adomainList': [ + 'ssmas.com' + ], + 'attrList': [] + } + ], + 'seat': null, + 'group': 0 + } + ], + 'bidid': '408731cc-c018-4976-bfc6-89f9c61e97a0', + 'cur': 'EUR', + 'nbr': -1 + }; + let bidResponse = { + 'mediaType': 'banner', + 'ad': '', + 'requestId': '37c658fe8ba57b', + 'seatBidId': '0001', + 'cpm': 10, + 'currency': 'EUR', + 'width': 300, + 'height': 250, + 'dealId': null, + 'creative_id': '3547894', + 'creativeId': '3547894', + 'ttl': 30, + 'netRevenue': true, + 'meta': { + 'advertiserDomains': [ + 'ssmas.com' + ] + } + }; + let bidRequest = { + 'imp': [ + { + 'ext': { + 'tid': '937db9c3-c22d-4454-b786-fcad76a349e5', + 'data': { + 'pbadslot': 'test-div' + } + }, + 'id': '3919400af0b73e8', + 'banner': { + 'topframe': 1, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ] + } + }, + { + 'ext': { + 'tid': '0c0d3d1b-0ad0-4786-896d-24c15fc6531d', + 'data': { + 'pbadslot': 'test-div2' + } + }, + 'id': '3919400af0b73e8', + 'banner': { + 'topframe': 1, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ] + } + } + ], + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/noadserver/basic_noadserver.html', + 'ref': 'http://localhost:9999/integrationExamples/noadserver/', + 'id': 1, + 'ext': { + 'placementId': 13144370 + } + }, + 'device': { + 'w': 1536, + 'h': 711, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0', + 'language': 'es' + }, + 'id': '8cc2f4b0-084d-4f40-acfa-5bec2023b1ab', + 'test': 0, + 'tmax': 20000, + 'source': { + 'tid': '8cc2f4b0-084d-4f40-acfa-5bec2023b1ab' + } + } + }); + + describe('test onBidWon function', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon({}); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(false); + }); + }); +}); diff --git a/test/spec/modules/sspBCBidAdapter_spec.js b/test/spec/modules/sspBCBidAdapter_spec.js index a95f08314b5..71619424e4b 100644 --- a/test/spec/modules/sspBCBidAdapter_spec.js +++ b/test/spec/modules/sspBCBidAdapter_spec.js @@ -38,7 +38,7 @@ describe('SSPBC adapter', function () { }, auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }, { @@ -60,7 +60,7 @@ describe('SSPBC adapter', function () { }, auctionId, bidderRequestId, - bidId: auctionId + '2', + bidId: bidderRequestId + '2', transactionId, } ]; @@ -83,7 +83,7 @@ describe('SSPBC adapter', function () { ], auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }; const bid_native = { @@ -122,7 +122,7 @@ describe('SSPBC adapter', function () { ], auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }; const bid_video = { @@ -144,7 +144,7 @@ describe('SSPBC adapter', function () { ], auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }; const bids_timeouted = [{ @@ -155,7 +155,7 @@ describe('SSPBC adapter', function () { siteId: '8816', }], auctionId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', timeout: 100, }, { @@ -166,7 +166,7 @@ describe('SSPBC adapter', function () { siteId: '8816', }], auctionId, - bidId: auctionId + '2', + bidId: bidderRequestId + '2', timeout: 100, } ]; @@ -198,7 +198,7 @@ describe('SSPBC adapter', function () { }, auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }]; const bidRequest = { @@ -293,7 +293,7 @@ describe('SSPBC adapter', function () { }; const serverResponse = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', @@ -333,7 +333,7 @@ describe('SSPBC adapter', function () { }; const serverResponseSingle = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', @@ -358,11 +358,11 @@ describe('SSPBC adapter', function () { }; const serverResponseOneCode = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', - 'impid': 'bidid-' + auctionId + '1', + 'impid': 'bidid-' + bidderRequestId + '1', 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', @@ -385,11 +385,11 @@ describe('SSPBC adapter', function () { }; const serverResponseVideo = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', - 'impid': 'bidid-' + auctionId + '1', + 'impid': 'bidid-' + bidderRequestId + '1', 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', @@ -413,11 +413,11 @@ describe('SSPBC adapter', function () { }; const serverResponseNative = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', - 'impid': 'bidid-' + auctionId + '1', + 'impid': 'bidid-' + bidderRequestId + '1', 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', @@ -438,7 +438,7 @@ describe('SSPBC adapter', function () { }; const emptyResponse = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, } } return { @@ -696,7 +696,7 @@ describe('SSPBC adapter', function () { let notificationPayload = spec.onBidWon(bid); expect(notificationPayload).to.have.property('event').that.equals('bidWon'); - expect(notificationPayload).to.have.property('requestId').that.equals(bid.auctionId); + expect(notificationPayload).to.have.property('requestId').that.equals(bid.bidderRequestId); expect(notificationPayload).to.have.property('tagid').that.deep.equals([bid.adUnitCode]); expect(notificationPayload).to.have.property('siteId').that.is.an('array'); expect(notificationPayload).to.have.property('slotId').that.is.an('array'); @@ -717,7 +717,6 @@ describe('SSPBC adapter', function () { let notificationPayload = spec.onTimeout(bids_timeouted); expect(notificationPayload).to.have.property('event').that.equals('timeout'); - expect(notificationPayload).to.have.property('requestId').that.equals(bids_timeouted[0].auctionId); expect(notificationPayload).to.have.property('tagid').that.deep.equals([bids_timeouted[0].adUnitCode, bids_timeouted[1].adUnitCode]); }); }); diff --git a/test/spec/modules/stroeerCoreBidAdapter_spec.js b/test/spec/modules/stroeerCoreBidAdapter_spec.js index 55d79804a38..2ed5f80f152 100644 --- a/test/spec/modules/stroeerCoreBidAdapter_spec.js +++ b/test/spec/modules/stroeerCoreBidAdapter_spec.js @@ -3,6 +3,7 @@ import {spec} from 'modules/stroeerCoreBidAdapter.js'; import * as utils from 'src/utils.js'; import {BANNER, VIDEO} from '../../../src/mediaTypes.js'; import {find} from 'src/polyfill.js'; +import sinon from 'sinon'; describe('stroeerCore bid adapter', function () { let sandbox; @@ -51,8 +52,6 @@ describe('stroeerCore bid adapter', function () { assert.notProperty(bidObject, 'ad'); } - const AUCTION_ID = utils.getUniqueIdentifierStr(); - // Vendor user ids and associated data const userIds = Object.freeze({ criteoId: 'criteo-user-id', @@ -72,7 +71,6 @@ describe('stroeerCore bid adapter', function () { }); const buildBidderRequest = () => ({ - auctionId: AUCTION_ID, bidderRequestId: 'bidder-request-id-123', bidderCode: 'stroeerCore', timeout: 5000, @@ -394,6 +392,10 @@ describe('stroeerCore bid adapter', function () { clock.tick(13500); const bidReq = buildBidderRequest(); + const UUID = 'fb6a39e3-083f-424c-9046-f1095e15f3d5'; + + const generateUUIDStub = sinon.stub(utils, 'generateUUID').returns(UUID); + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); const expectedTimeout = bidderRequest.timeout - (13500 - bidderRequest.auctionStart); @@ -401,7 +403,7 @@ describe('stroeerCore bid adapter', function () { assert.equal(expectedTimeout, 1500); const expectedJsonPayload = { - 'id': AUCTION_ID, + 'id': UUID, 'timeout': expectedTimeout, 'ref': 'https://www.example.com/?search=monkey', 'mpa': true, @@ -429,8 +431,9 @@ describe('stroeerCore bid adapter', function () { // trim away fields with undefined const actualJsonPayload = JSON.parse(JSON.stringify(serverRequestInfo.data)); - assert.deepEqual(actualJsonPayload, expectedJsonPayload); + + generateUUIDStub.restore(); }); describe('video bids', () => { diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 7d31e291667..16bbb525ee7 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -173,7 +173,7 @@ describe('Taboola Adapter', function () { const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); - expect(res.url).to.equal(`${END_POINT_URL}/${commonBidRequest.params.publisherId}`); + expect(res.url).to.equal(`${END_POINT_URL}?publisher=${commonBidRequest.params.publisherId}`); expect(res.data).to.deep.equal(JSON.stringify(expectedData)); }); diff --git a/test/spec/modules/tappxBidAdapter_spec.js b/test/spec/modules/tappxBidAdapter_spec.js index 58a62fb2869..46fac8de1e2 100644 --- a/test/spec/modules/tappxBidAdapter_spec.js +++ b/test/spec/modules/tappxBidAdapter_spec.js @@ -466,4 +466,23 @@ describe('Tappx bid adapter', function () { assert.isString(_extractPageUrl(validBidRequests, bidderRequest)); }); }) + + describe('Empty params values from bid tests', function() { + let validBidRequest = JSON.parse(JSON.stringify(c_BIDREQUEST)); + + it('should return false when tappxkey is empty', function () { + validBidRequest.bids[0].params.tappxkey = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + + it('should return false when host is empty', function () { + validBidRequest.bids[0].params.host = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + + it('should return false when endpoint is empty', function () { + validBidRequest.bids[0].params.endpoint = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + }); }); diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index a4b94135bad..b0d5f436e0b 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -1,23 +1,20 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/teadsBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; -import {getStorageManager} from 'src/storageManager'; const ENDPOINT = 'https://a.teads.tv/hb/bid-request'; const AD_SCRIPT = '"'; describe('teadsBidAdapter', () => { const adapter = newBidder(spec); - let cookiesAreEnabledStub, getCookieStub; + let sandbox; beforeEach(function () { - cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); - getCookieStub = sinon.stub(storage, 'getCookie'); + sandbox = sinon.sandbox.create(); }); afterEach(function () { - cookiesAreEnabledStub.restore(); - getCookieStub.restore(); + sandbox.restore(); }); describe('inherited functions', () => { @@ -108,21 +105,21 @@ describe('teadsBidAdapter', () => { } ]; - let bidderResquestDefault = { + let bidderRequestDefault = { 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000 }; it('should send bid request to ENDPOINT via POST', function() { - const request = spec.buildRequests(bidRequests, bidderResquestDefault); + const request = spec.buildRequests(bidRequests, bidderRequestDefault); expect(request.url).to.equal(ENDPOINT); expect(request.method).to.equal('POST'); }); it('should not send auctionId in bid request ', function() { - const request = spec.buildRequests(bidRequests, bidderResquestDefault); + const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload.data[0].auctionId).to.not.exist @@ -169,6 +166,55 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab.apiVersion).to.equal(2); }); + it('should add videoPlcmt to payload', function () { + let bidRequestWithVideoPlcmt = Object.assign({}, bidRequests[0], { + mediaTypes: { + video: { + plcmt: 1 + } + } + }); + + const request = spec.buildRequests([bidRequestWithVideoPlcmt], bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].videoPlcmt).to.exist; + expect(payload.data[0].videoPlcmt).to.equal(1); + }); + + it('should not add videoPlcmt to payload if empty', function () { + let bidRequestWithNullVideoPlcmt = Object.assign({}, bidRequests[0], { + mediaTypes: { + video: { + plcmt: null + } + } + }); + + let bidRequestWithEmptyVideoPlcmt = Object.assign({}, bidRequests[0], { + mediaTypes: { + video: { + plcmt: '' + } + } + }); + + const request1 = spec.buildRequests([bidRequestWithNullVideoPlcmt], bidderRequestDefault); + const request2 = spec.buildRequests([bidRequestWithEmptyVideoPlcmt], bidderRequestDefault); + const payload1 = JSON.parse(request1.data); + const payload2 = JSON.parse(request2.data); + + expect(payload1.data[0].videoPlcmt).to.not.exist; + expect(payload2.data[0].videoPlcmt).to.not.exist; + }); + + it('should not add videoPlcmt to payload if it is not in bid request', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].videoPlcmt).to.not.exist; + }); + it('should add referer info to payload', function () { const bidRequest = Object.assign({}, bidRequests[0]) const bidderRequest = { @@ -186,7 +232,7 @@ describe('teadsBidAdapter', () => { }); it('should add networkBandwidth info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderResquestDefault); + const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); const bandwidth = window.navigator && window.navigator.connection && window.navigator.connection.downlink; @@ -201,15 +247,110 @@ describe('teadsBidAdapter', () => { }); it('should add pageReferrer info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderResquestDefault); + const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload.pageReferrer).to.exist; expect(payload.pageReferrer).to.deep.equal(document.referrer); }); + describe('pageTitle', function () { + it('should add pageTitle info to payload based on document title', function () { + const testText = 'This is a title'; + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload based on open-graph title', function () { + const testText = 'This is a title from open-graph'; + sandbox.stub(window.top.document, 'title').value(''); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.have.length(300); + }); + + it('should add pageTitle info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback title'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + }); + + describe('pageDescription', function () { + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description from open-graph'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.have.length(300); + }); + + it('should add pageDescription info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback description'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + }); + it('should add timeToFirstByte info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderResquestDefault); + const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); const performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance; @@ -410,7 +551,7 @@ describe('teadsBidAdapter', () => { } }); - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload.schain).to.exist; @@ -458,7 +599,7 @@ describe('teadsBidAdapter', () => { } }); - const requestWithUserAgentClientHints = spec.buildRequests([bidRequest], bidderResquestDefault); + const requestWithUserAgentClientHints = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(requestWithUserAgentClientHints.data); expect(payload.userAgentClientHints).to.exist; @@ -489,7 +630,7 @@ describe('teadsBidAdapter', () => { } ); - const defaultRequest = spec.buildRequests(bidRequests, bidderResquestDefault); + const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); expect(JSON.parse(defaultRequest.data).userAgentClientHints).to.not.exist; }); @@ -566,7 +707,7 @@ describe('teadsBidAdapter', () => { userId: {} // no property -> assumption that the system is disabled }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); for (const userId in userIdModules) { @@ -575,7 +716,7 @@ describe('teadsBidAdapter', () => { }); it(`should not add param to payload if user id field is absent`, function () { - const request = spec.buildRequests([baseBidRequest], bidderResquestDefault); + const request = spec.buildRequests([baseBidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); for (const userId in userIdModules) { @@ -592,7 +733,7 @@ describe('teadsBidAdapter', () => { } }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload).not.to.have.property('liveRampId'); @@ -612,7 +753,7 @@ describe('teadsBidAdapter', () => { userId: userIdObject }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload['unifiedId2']).to.equal('unifiedId2-id'); @@ -632,7 +773,7 @@ describe('teadsBidAdapter', () => { describe('First-party cookie Teads ID', function () { it('should not add firstPartyCookieTeadsId param to payload if cookies are not enabled' + ' and teads user id not available', function () { - cookiesAreEnabledStub.returns(false); + sandbox.stub(storage, 'cookiesAreEnabled').returns(false); const bidRequest = { ...baseBidRequest, @@ -641,7 +782,7 @@ describe('teadsBidAdapter', () => { } }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload).not.to.have.property('firstPartyCookieTeadsId'); @@ -649,8 +790,8 @@ describe('teadsBidAdapter', () => { it('should not add firstPartyCookieTeadsId param to payload if cookies are enabled ' + 'but first-party cookie and teads user id are not available', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns(undefined); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns(undefined); const bidRequest = { ...baseBidRequest, @@ -659,7 +800,7 @@ describe('teadsBidAdapter', () => { } }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload).not.to.have.property('firstPartyCookieTeadsId'); @@ -667,8 +808,8 @@ describe('teadsBidAdapter', () => { it('should add firstPartyCookieTeadsId from cookie if it\'s available ' + 'and teads user id is not', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns('my-teads-id'); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns('my-teads-id'); const bidRequest = { ...baseBidRequest, @@ -677,7 +818,7 @@ describe('teadsBidAdapter', () => { } }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); @@ -686,8 +827,8 @@ describe('teadsBidAdapter', () => { it('should add firstPartyCookieTeadsId from user id module if it\'s available ' + 'even if cookie is available too', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns('my-teads-id'); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns('my-teads-id'); const bidRequest = { ...baseBidRequest, @@ -697,7 +838,7 @@ describe('teadsBidAdapter', () => { } }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); @@ -750,7 +891,7 @@ describe('teadsBidAdapter', () => { }; } ); - const request = spec.buildRequests(updatedBidRequests, bidderResquestDefault); + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload.data[0].gpid).to.equal('1111/home-left-0'); @@ -767,7 +908,7 @@ describe('teadsBidAdapter', () => { } })); - const request = spec.buildRequests(updatedBidRequests, bidderResquestDefault); + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); return payload.data.forEach(bid => { @@ -784,7 +925,7 @@ describe('teadsBidAdapter', () => { } })); - const request = spec.buildRequests(updatedBidRequests, bidderResquestDefault); + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); return payload.data.forEach(bid => { @@ -795,7 +936,7 @@ describe('teadsBidAdapter', () => { function checkMediaTypesSizes(mediaTypes, expectedSizes) { const bidRequestWithBannerSizes = Object.assign(bidRequests[0], mediaTypes); - const requestWithBannerSizes = spec.buildRequests([bidRequestWithBannerSizes], bidderResquestDefault); + const requestWithBannerSizes = spec.buildRequests([bidRequestWithBannerSizes], bidderRequestDefault); const payloadWithBannerSizes = JSON.parse(requestWithBannerSizes.data); return payloadWithBannerSizes.data.forEach(bid => { diff --git a/test/spec/modules/teadsIdSystem_spec.js b/test/spec/modules/teadsIdSystem_spec.js index 7b977e2fb2b..1959b990957 100644 --- a/test/spec/modules/teadsIdSystem_spec.js +++ b/test/spec/modules/teadsIdSystem_spec.js @@ -218,7 +218,7 @@ describe('TeadsIdSystem', function () { callback(callbackSpy); const request = server.requests[0]; expect(request.url).to.include(teadsUrl); - request.respond(204, null, 'Unavailable'); + request.respond(204); expect(logInfoStub.calledOnce).to.be.true; }); diff --git a/test/spec/modules/topicsFpdModule_spec.js b/test/spec/modules/topicsFpdModule_spec.js index 958489f2728..bc7df85db0d 100644 --- a/test/spec/modules/topicsFpdModule_spec.js +++ b/test/spec/modules/topicsFpdModule_spec.js @@ -1,289 +1,313 @@ import { + getCachedTopics, getTopics, getTopicsData, + loadTopicsForBidders, processFpd, - hasGDPRConsent, - getCachedTopics, receiveMessage, + reset, topicStorageName } from '../../../modules/topicsFpdModule.js'; +import {config} from 'src/config.js'; import {deepClone, safeJSONParse} from '../../../src/utils.js'; -import {gdprDataHandler} from 'src/adapterManager.js'; import {getCoreStorageManager} from 'src/storageManager.js'; +import * as activities from '../../../src/activities/rules.js'; +import {ACTIVITY_ENRICH_UFPD} from '../../../src/activities/activities.js'; -describe('getTopicsData', () => { - function makeTopic(topic, modelv, taxv = '1') { - return { - topic, - taxonomyVersion: taxv, - modelVersion: modelv - }; - } - - function byTaxClass(segments) { - return segments.reduce((memo, segment) => { - memo[`${segment.ext.segtax}:${segment.ext.segclass}`] = segment; - return memo; - }, {}); - } - - [ - { - t: 'no topics', - topics: [], - expected: [] - }, - { - t: 'single topic', - topics: [makeTopic(123, 'm1')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '123'} - ] - } - ] - }, - { - t: 'multiple topics with the same model version', - topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '123'}, - {id: '321'} - ] - } - ] - }, - { - t: 'multiple topics with different model versions', - topics: [makeTopic(1, 'm1'), makeTopic(2, 'm1'), makeTopic(3, 'm2')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '1'}, - {id: '2'} - ] - }, - { - ext: { - segtax: 600, - segclass: 'm2' - }, - segment: [ - {id: '3'} - ] - } - ] - }, - { - t: 'multiple topics, some with a taxonomy version other than "1"', - topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1', 'other')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '123'} - ] - } - ] - }, - { - t: 'multiple topics in multiple taxonomies', - taxonomies: { - '1': 600, - '2': 601 +describe('topics', () => { + beforeEach(() => { + reset(); + }); + + describe('getTopicsData', () => { + function makeTopic(topic, modelv, taxv = '1') { + return { + topic, + taxonomyVersion: taxv, + modelVersion: modelv + }; + } + + function byTaxClass(segments) { + return segments.reduce((memo, segment) => { + memo[`${segment.ext.segtax}:${segment.ext.segclass}`] = segment; + return memo; + }, {}); + } + + [ + { + t: 'no topics', + topics: [], + expected: [] }, - topics: [ - makeTopic(123, 'm1', '1'), - makeTopic(321, 'm1', '2'), - makeTopic(213, 'm2', '1'), - ], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' + { + t: 'single topic', + topics: [makeTopic(123, 'm1')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] + } + ] + }, + { + t: 'multiple topics with the same model version', + topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'}, + {id: '321'} + ] + } + ] + }, + { + t: 'multiple topics with different model versions', + topics: [makeTopic(1, 'm1'), makeTopic(2, 'm1'), makeTopic(3, 'm2')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '1'}, + {id: '2'} + ] }, - segment: [ - {id: '123'} - ] + { + ext: { + segtax: 600, + segclass: 'm2' + }, + segment: [ + {id: '3'} + ] + } + ] + }, + { + t: 'multiple topics, some with a taxonomy version other than "1"', + topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1', 'other')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] + } + ] + }, + { + t: 'multiple topics in multiple taxonomies', + taxonomies: { + '1': 600, + '2': 601 }, - { - ext: { - segtax: 601, - segclass: 'm1', + topics: [ + makeTopic(123, 'm1', '1'), + makeTopic(321, 'm1', '2'), + makeTopic(213, 'm2', '1'), + ], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] }, - segment: [ - {id: '321'} - ] - }, - { - ext: { - segtax: 600, - segclass: 'm2' + { + ext: { + segtax: 601, + segclass: 'm1', + }, + segment: [ + {id: '321'} + ] }, - segment: [ - {id: '213'} - ] - } - ] - } - ].forEach(({t, topics, expected, taxonomies}) => { - describe(`on ${t}`, () => { - it('should convert topics to user.data segments correctly', () => { - const actual = getTopicsData('mockName', topics, taxonomies); - expect(actual.length).to.eql(expected.length); - expected = byTaxClass(expected); - Object.entries(byTaxClass(actual)).forEach(([key, datum]) => { - sinon.assert.match(datum, expected[key]); - expect(datum.name).to.equal('mockName'); + { + ext: { + segtax: 600, + segclass: 'm2' + }, + segment: [ + {id: '213'} + ] + } + ] + } + ].forEach(({t, topics, expected, taxonomies}) => { + describe(`on ${t}`, () => { + it('should convert topics to user.data segments correctly', () => { + const actual = getTopicsData('mockName', topics, taxonomies); + expect(actual.length).to.eql(expected.length); + expected = byTaxClass(expected); + Object.entries(byTaxClass(actual)).forEach(([key, datum]) => { + sinon.assert.match(datum, expected[key]); + expect(datum.name).to.equal('mockName'); + }); }); - }); - it('should not set name if null', () => { - getTopicsData(null, topics).forEach((data) => { - expect(data.hasOwnProperty('name')).to.be.false; + it('should not set name if null', () => { + getTopicsData(null, topics).forEach((data) => { + expect(data.hasOwnProperty('name')).to.be.false; + }); }); }); }); }); -}); -describe('getTopics', () => { - Object.entries({ - 'document with no browsingTopics': {}, - 'document that disallows topics': { - featurePolicy: { - allowsFeature: sinon.stub().returns(false) - } - }, - 'document that throws on featurePolicy': { - browsingTopics: sinon.stub(), - get featurePolicy() { - throw new Error(); - } - }, - 'document that throws on browsingTopics': { - browsingTopics: sinon.stub().callsFake(() => { - throw new Error(); - }), - featurePolicy: { - allowsFeature: sinon.stub().returns(true) - } - }, - }).forEach(([t, doc]) => { - it(`should resolve to an empty list on ${t}`, () => { - return getTopics(doc).then((topics) => { - expect(topics).to.eql([]); + describe('getTopics', () => { + Object.entries({ + 'document with no browsingTopics': {}, + 'document that disallows topics': { + featurePolicy: { + allowsFeature: sinon.stub().returns(false) + } + }, + 'document that throws on featurePolicy': { + browsingTopics: sinon.stub(), + get featurePolicy() { + throw new Error(); + } + }, + 'document that throws on browsingTopics': { + browsingTopics: sinon.stub().callsFake(() => { + throw new Error(); + }), + featurePolicy: { + allowsFeature: sinon.stub().returns(true) + } + }, + }).forEach(([t, doc]) => { + it(`should resolve to an empty list on ${t}`, () => { + return getTopics(doc).then((topics) => { + expect(topics).to.eql([]); + }); }); }); - }); - it('should call `document.browsingTopics` when allowed', () => { - const topics = ['t1', 't2']; - return getTopics({ - browsingTopics: sinon.stub().returns(Promise.resolve(topics)), - featurePolicy: { - allowsFeature: sinon.stub().returns(true) - } - }).then((actual) => { - expect(actual).to.eql(topics); - }); - }); -}); - -describe('processFpd', () => { - const mockData = [ - { - name: 'domain', - segment: [{id: 123}] - }, - { - name: 'domain', - segment: [{id: 321}] - } - ]; - - it('should add topics data', () => { - return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) - .then(({global}) => { - expect(global.user.data).to.eql(mockData); + it('should call `document.browsingTopics` when allowed', () => { + const topics = ['t1', 't2']; + return getTopics({ + browsingTopics: sinon.stub().returns(Promise.resolve(topics)), + featurePolicy: { + allowsFeature: sinon.stub().returns(true) + } + }).then((actual) => { + expect(actual).to.eql(topics); }); + }); }); - it('should apppend to existing user.data', () => { - const global = { - user: { - data: [ - {name: 'preexisting'}, - ] + describe('processFpd', () => { + const mockData = [ + { + name: 'domain', + segment: [{id: 123}] + }, + { + name: 'domain', + segment: [{id: 321}] } - }; - return processFpd({}, {global: deepClone(global)}, {data: Promise.resolve(mockData)}) - .then((data) => { - expect(data.global.user.data).to.eql(global.user.data.concat(mockData)); - }); - }); - - it('should not modify fpd when there is no data', () => { - return processFpd({}, {global: {}}, {data: Promise.resolve([])}) - .then((data) => { - expect(data.global).to.eql({}); - }); - }); -}); + ]; -describe('Topics Module GDPR consent check', () => { - let gdprDataHdlrStub; - beforeEach(() => { - gdprDataHdlrStub = sinon.stub(gdprDataHandler, 'getConsentData'); - }); + it('should add topics data', () => { + return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) + .then(({global}) => { + expect(global.user.data).to.eql(mockData); + }); + }); - afterEach(() => { - gdprDataHdlrStub.restore(); - }); + it('should apppend to existing user.data', () => { + const global = { + user: { + data: [ + {name: 'preexisting'}, + ] + } + }; + return processFpd({}, {global: deepClone(global)}, {data: Promise.resolve(mockData)}) + .then((data) => { + expect(data.global.user.data).to.eql(global.user.data.concat(mockData)); + }); + }); - it('should return false when GDPR is applied but consent string is not present', () => { - const consentString = ''; - const consentConfig = { - consentString: consentString, - gdprApplies: true, - vendorData: {} - }; - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(false); + it('should not modify fpd when there is no data', () => { + return processFpd({}, {global: {}}, {data: Promise.resolve([])}) + .then((data) => { + expect(data.global).to.eql({}); + }); + }); }); - it('should return true when GDPR doesn\'t apply', () => { - const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; - const consentConfig = { - consentString: consentString, - gdprApplies: false, - vendorData: {} - }; + describe('loadTopicsForBidders', () => { + beforeEach(() => { + config.setConfig({ + userSync: { + topics: { + bidders: [{ + bidder: 'mockBidder', + iframeURL: 'https://mock.iframe' + }] + } + } + }) + }); + afterEach(() => { + config.resetConfig(); + }) - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(true); + Object.entries({ + 'support': {}, + 'allow': { + browsingTopics: true, + featurePolicy: { + allowsFeature(feature) { + return feature !== 'browsing-topics'; + } + } + }, + }).forEach(([t, doc]) => { + it(`does not attempt to load frames if browser does not ${t} topics`, () => { + doc.createElement = sinon.stub(); + loadTopicsForBidders(doc); + sinon.assert.notCalled(doc.createElement); + }); + }); }); - it('should return true when GDPR is applied and purpose consent is true for all purpose[1,2,3,4]', () => { + describe('getCachedTopics()', () => { + const storage = getCoreStorageManager('topicsFpd'); + const expected = [{ + ext: { + segtax: 600, + segclass: '2206021246' + }, + segment: [{ + 'id': '243' + }, { + 'id': '265' + }], + name: 'ads.pubmatic.com' + }]; const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; const consentConfig = { consentString: consentString, @@ -301,93 +325,159 @@ describe('Topics Module GDPR consent check', () => { } } }; + const mockData = [ + { + name: 'domain', + segment: [{id: 123}] + }, + { + name: 'domain', + segment: [{id: 321}], + } + ]; - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(true); - }); + const evt = { + data: '{"segment":{"domain":"ads.pubmatic.com","topics":[{"configVersion":"chrome.1","modelVersion":"2206021246","taxonomyVersion":"1","topic":165,"version":"chrome.1:1:2206021246"}],"bidder":"pubmatic"},"date":1669743901858}', + origin: 'https://ads.pubmatic.com' + }; - it('should return false when GDPR is applied and purpose consent is false for one of the purpose[1,2,3,4]', () => { - const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; - const consentConfig = { - consentString: consentString, - gdprApplies: true, - vendorData: { - metadata: consentString, - gdprApplies: true, - purpose: { - consents: { - 1: true, - 2: true, - 3: true, - 4: false + afterEach(() => { + storage.removeDataFromLocalStorage(topicStorageName); + }); + + describe('when cached data is available and not expired', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + const storedSegments = JSON.stringify( + [['pubmatic', { + '2206021246': { + 'ext': {'segtax': 600, 'segclass': '2206021246'}, + 'segment': [{'id': '243'}, {'id': '265'}], + 'name': 'ads.pubmatic.com' + }, + 'lastUpdated': new Date().getTime() + }]] + ); + storage.setDataInLocalStorage(topicStorageName, storedSegments); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('should return segments for bidder if transmitUfpd is allowed', () => { + assert.deepEqual(getCachedTopics(), expected); + }); + + it('should NOT return segments for bidder if enrichUfpd is NOT allowed', () => { + sandbox.stub(activities, 'isActivityAllowed').callsFake((activity, params) => { + return !(activity === ACTIVITY_ENRICH_UFPD && params.component === 'bidder.pubmatic'); + }); + expect(getCachedTopics()).to.eql([]); + }); + }) + + it('should return empty segments for bidder if there is cached segments stored which is expired', () => { + let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":10}]]'; + storage.setDataInLocalStorage(topicStorageName, storedSegments); + assert.deepEqual(getCachedTopics(), []); + }); + + describe('cross-frame messages', () => { + beforeEach(() => { + // init iframe logic so that the receiveMessage origin check passes + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + }, + createElement: sinon.stub().callsFake(() => ({style: {}})), + documentElement: { + appendChild() {} } - } - } - }; + }); + }); + + it('should store segments if receiveMessage event is triggered with segment data', () => { + receiveMessage(evt); + let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); + expect(segments.has('pubmatic')).to.equal(true); + }); - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(false); + it('should update stored segments if receiveMessage event is triggerred with segment data', () => { + let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":1669719242027}]]'; + storage.setDataInLocalStorage(topicStorageName, storedSegments); + receiveMessage(evt); + let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); + expect(segments.get('pubmatic')[2206021246].segment.length).to.equal(1); + }); + }); }); }); -describe('getCachedTopics()', () => { +describe('handles fetch request for topics api headers', () => { + let stubbedFetch; const storage = getCoreStorageManager('topicsFpd'); - const expected = [{ - ext: { - segtax: 600, - segclass: '2206021246' - }, - segment: [{ - 'id': '243' - }, { - 'id': '265' - }], - name: 'ads.pubmatic.com' - }]; - const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; - const consentConfig = { - consentString: consentString, - gdprApplies: true, - vendorData: { - metadata: consentString, - gdprApplies: true, - purpose: { - consents: { - 1: true, - 2: true, - 3: true, - 4: true - } - } - } - }; - const mockData = [ - { - name: 'domain', - segment: [{id: 123}] - }, - { - name: 'domain', - segment: [{id: 321}], - } - ]; - - const evt = { - data: '{"segment":{"domain":"ads.pubmatic.com","topics":[{"configVersion":"chrome.1","modelVersion":"2206021246","taxonomyVersion":"1","topic":165,"version":"chrome.1:1:2206021246"}],"bidder":"pubmatic"},"date":1669743901858}', - origin: 'https://ads.pubmatic.com' - }; - let gdprDataHdlrStub; beforeEach(() => { - gdprDataHdlrStub = sinon.stub(gdprDataHandler, 'getConsentData'); + stubbedFetch = sinon.stub(window, 'fetch'); }); afterEach(() => { + stubbedFetch.restore(); storage.removeDataFromLocalStorage(topicStorageName); - gdprDataHdlrStub.restore(); }); - it('should return segments for bidder if GDPR consent is true and there is cached segments stored which is not expired', () => { + it('should make a fetch call when a fetchUrl is present for a selected bidder', () => { + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic', + fetchUrl: 'http://localhost:3000/topics-server.js' + } + ], + }, + } + }); + + stubbedFetch.returns(Promise.resolve(true)); + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.calledOnce(stubbedFetch); + stubbedFetch.calledWith('http://localhost:3000/topics-server.js'); + }); + + it('should not make a fetch call when a fetchUrl is not present for a selected bidder', () => { + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic' + } + ], + }, + } + }); + + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.notCalled(stubbedFetch); + }); + + it('a fetch request should not be made if the configured fetch rate duration has not yet passed', () => { const storedSegments = JSON.stringify( [['pubmatic', { '2206021246': { @@ -398,35 +488,30 @@ describe('getCachedTopics()', () => { 'lastUpdated': new Date().getTime() }]] ); - storage.setDataInLocalStorage(topicStorageName, storedSegments); - gdprDataHdlrStub.returns(consentConfig); - assert.deepEqual(getCachedTopics(), expected); - }); - it('should return empty segments for bidder if GDPR consent is true and there is cached segments stored which is expired', () => { - let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":10}]]'; storage.setDataInLocalStorage(topicStorageName, storedSegments); - gdprDataHdlrStub.returns(consentConfig); - assert.deepEqual(getCachedTopics(), []); - }); - it('should stored segments if receiveMessage event is triggerred with segment data', () => { - return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) - .then(({global}) => { - receiveMessage(evt); - let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); - expect(segments.has('pubmatic')).to.equal(true); - }); - }); + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic', + fetchUrl: 'http://localhost:3000/topics-server.js', + fetchRate: 1 // in days. 1 fetch per day + } + ], + }, + } + }); - it('should update stored segments if receiveMessage event is triggerred with segment data', () => { - let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":1669719242027}]]'; - storage.setDataInLocalStorage(topicStorageName, storedSegments); - return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) - .then(({global}) => { - receiveMessage(evt); - let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); - expect(segments.get('pubmatic')[2206021246].segment.length).to.equal(1); - }); + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.notCalled(stubbedFetch); }); }); diff --git a/test/spec/modules/tpmnBidAdapter_spec.js b/test/spec/modules/tpmnBidAdapter_spec.js index e2b14b18f61..505bc9d878f 100644 --- a/test/spec/modules/tpmnBidAdapter_spec.js +++ b/test/spec/modules/tpmnBidAdapter_spec.js @@ -1,16 +1,130 @@ /* eslint-disable no-tabs */ -import {expect} from 'chai'; -import {spec, storage} from 'modules/tpmnBidAdapter.js'; -import {generateUUID} from '../../../src/utils.js'; -import {newBidder} from '../../../src/adapters/bidderFactory'; +import { spec, storage, VIDEO_RENDERER_URL, ADAPTER_VERSION } from 'modules/tpmnBidAdapter.js'; +import { generateUUID } from '../../../src/utils.js'; +import { expect } from 'chai'; +import * as utils from 'src/utils'; import * as sinon from 'sinon'; +import 'modules/consentManagement.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {mockGdprConsent} from '../../helpers/consentData.js'; + +const BIDDER_CODE = 'tpmn'; +const BANNER_BID = { + bidder: BIDDER_CODE, + params: { + inventoryId: 1 + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ], + }, + }, + adUnitCode: 'adUnitCode1', + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', +}; + +const VIDEO_BID = { + bidder: BIDDER_CODE, + params: { + inventoryId: 1 + }, + mediaTypes: { + video: { + context: 'outstream', + api: [1, 2, 4, 6], + mimes: ['video/mp4'], + playbackmethod: [2, 4, 6], + playerSize: [[1024, 768]], + protocols: [3, 4, 7, 8, 10], + placement: 1, + plcmt: 1, + minduration: 0, + maxduration: 60, + startdelay: 0 + }, + }, + adUnitCode: 'adUnitCode1', + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', +}; + +const BIDDER_REQUEST = { + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + bidderRequestId: 'bidderRequestId', + timeout: 500, + refererInfo: { + page: 'https://hello-world-page.com/', + domain: 'hello-world-page.com', + ref: 'http://example-domain.com/foo', + } +}; + +const BANNER_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidId': 'bidid', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'id', + 'impid': 'bidId', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'burl': 'http://0.0.0.0:8181/burl', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'w': 300, + 'h': 250 + } + ] + } + ], + 'cur': 'USD' +}; + +const VIDEO_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidid': 'bidid', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'id', + 'impid': 'bidId', + 'price': 1.09, + 'adid': '144762342', + 'burl': 'http://0.0.0.0:8181/burl', + 'adm': '', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'h': 768, + 'w': 1024 + } + ] + } + ], + 'cur': 'USD' +}; describe('tpmnAdapterTests', function () { - const adapter = newBidder(spec); - const BIDDER_CODE = 'tpmn'; let sandbox = sinon.sandbox.create(); let getCookieStub; - beforeEach(function () { $$PREBID_GLOBAL$$.bidderSettings = { tpmn: { @@ -27,152 +141,277 @@ describe('tpmnAdapterTests', function () { $$PREBID_GLOBAL$$.bidderSettings = {}; }); - describe('inherited functions', function () { - it('exists and is a function', function () { - expect(adapter.callBids).to.exist.and.to.be.a('function') - }) - }); - - describe('isBidRequestValid', function () { - let bid = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', - params: { - inventoryId: '1', - publisherId: 'TPMN' - }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', - mediaTypes: { - banner: { - sizes: [[300, 250]] - } - } - }; - - it('should return true if a bid is valid banner bid request', function () { - expect(spec.isBidRequestValid(bid)).to.be.equal(true); - }); - - it('should return false where requried param is missing', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.be.equal(false); - }); - - it('should return false when required param values have invalid type', function () { - let bid = Object.assign({}, bid); - bid.params = { - 'inventoryId': null, - 'publisherId': null - }; - expect(spec.isBidRequestValid(bid)).to.be.equal(false); - }); - }); - - describe('buildRequests', function () { - it('should return an empty list if there are no bid requests', function () { - const emptyBidRequests = []; - const bidderRequest = {}; - const request = spec.buildRequests(emptyBidRequests, bidderRequest); - expect(request).to.be.an('array').that.is.empty; - }); - it('should generate a POST server request with bidder API url, data', function () { - const bid = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', + describe('isBidRequestValid()', function () { + it('should accept request if placementId is passed', function () { + let bid = { + bidder: BIDDER_CODE, params: { - inventoryId: '1', - publisherId: 'TPMN' + inventoryId: 123 }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', mediaTypes: { banner: { sizes: [[300, 250]] } } }; - const tempBidRequests = [bid]; - const tempBidderRequest = { - refererInfo: { - page: 'http://localhost/test', - site: { - domain: 'localhost', - page: 'http://localhost/test' - } - } + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should reject requests without params', function () { + let bid = { + bidder: BIDDER_CODE, + params: {} }; - const builtRequest = spec.buildRequests(tempBidRequests, tempBidderRequest); - - expect(builtRequest).to.have.lengthOf(1); - expect(builtRequest[0].method).to.equal('POST'); - expect(builtRequest[0].url).to.match(/ad.tpmn.co.kr\/prebidhb.tpmn/); - const apiRequest = builtRequest[0].data; - expect(apiRequest.site).to.deep.equal({ - domain: 'localhost', - page: 'http://localhost/test' - }); - expect(apiRequest.bids).to.have.lengthOf('1'); - expect(apiRequest.bids[0].inventoryId).to.equal('1'); - expect(apiRequest.bids[0].publisherId).to.equal('TPMN'); - expect(apiRequest.bids[0].bidId).to.equal('29092404798c9'); - expect(apiRequest.bids[0].adUnitCode).to.equal('temp-unitcode'); - expect(apiRequest.bids[0].auctionId).to.equal('da1d7a33-0260-4e83-a621-14674116f3f9'); - expect(apiRequest.bids[0].sizes).to.have.lengthOf('1'); - expect(apiRequest.bids[0].sizes[0]).to.deep.equal({ - width: 300, - height: 250 - }); + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(BANNER_BID)).to.equal(true); + expect(spec.isBidRequestValid(VIDEO_BID)).to.equal(true); }); }); - describe('interpretResponse', function () { - const bid = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', - params: { - inventoryId: '1', - publisherId: 'TPMN' - }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', - mediaTypes: { - banner: { - sizes: [[300, 250]] + describe('buildRequests()', function () { + it('should have gdpr data if applicable', function () { + const bid = utils.deepClone(BANNER_BID); + + const req = syncAddFPDToBidderRequest(Object.assign({}, BIDDER_REQUEST, { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, } + })); + let request = spec.buildRequests([bid], req)[0]; + + const payload = request.data; + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + + it('should properly forward ORTB blocking params', function () { + let bid = utils.deepClone(BANNER_BID); + bid = utils.mergeDeep(bid, { + params: { bcat: ['IAB1-1'], badv: ['example.com'], bapp: ['com.example'], battr: [1] }, + mediaTypes: { banner: { battr: [1] } } + }); + + let [request] = spec.buildRequests([bid], BIDDER_REQUEST); + + expect(request).to.exist.and.to.be.an('object'); + const payload = request.data; + expect(payload).to.have.deep.property('bcat', ['IAB1-1']); + expect(payload).to.have.deep.property('badv', ['example.com']); + expect(payload).to.have.deep.property('bapp', ['com.example']); + expect(payload.imp[0].banner).to.have.deep.property('battr', [1]); + }); + + context('when mediaType is banner', function () { + it('should build correct request for banner bid with both w, h', () => { + const bid = utils.deepClone(BANNER_BID); + + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const requestData = request.data; + // expect(requestData.imp[0].banner).to.equal(null); + expect(requestData.imp[0].banner.format[0].w).to.equal(300); + expect(requestData.imp[0].banner.format[0].h).to.equal(250); + }); + + it('should create request data', function () { + const bid = utils.deepClone(BANNER_BID); + + let [request] = spec.buildRequests([bid], BIDDER_REQUEST); + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', bid.bidId); + }); + }); + + context('when mediaType is video', function () { + if (FEATURES.VIDEO) { + it('should return false when there is no video in mediaTypes', () => { + const bid = utils.deepClone(VIDEO_BID); + delete bid.mediaTypes.video; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); } - }; - const tempBidRequests = [bid]; - it('should return an empty aray to indicate no valid bids', function () { - const emptyServerResponse = {}; - const bidResponses = spec.interpretResponse(emptyServerResponse, tempBidRequests); - expect(bidResponses).is.an('array').that.is.empty; + if (FEATURES.VIDEO) { + it('should reutrn false if player size is not set', () => { + const bid = utils.deepClone(VIDEO_BID); + delete bid.mediaTypes.video.playerSize; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + } + if (FEATURES.VIDEO) { + it('when mediaType is Video - check', () => { + const bid = utils.deepClone(VIDEO_BID); + const check = { + w: 1024, + h: 768, + mimes: ['video/mp4'], + playbackmethod: [2, 4, 6], + api: [1, 2, 4, 6], + protocols: [3, 4, 7, 8, 10], + placement: 1, + minduration: 0, + maxduration: 60, + startdelay: 0, + plcmt: 1 + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + const requests = spec.buildRequests([bid], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video).to.deep.include({...check}); + }); + } + + if (FEATURES.VIDEO) { + it('when mediaType New Video', () => { + const NEW_VIDEO_BID = { + 'bidder': 'tpmn', + 'params': {'inventoryId': 2, 'bidFloor': 2}, + 'userId': {'pubcid': '88a49ee6-beeb-4dd6-92ac-3b6060e127e1'}, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': ['video/mp4'], + 'playerSize': [[1024, 768]], + 'playbackmethod': [2, 4, 6], + 'protocols': [3, 4], + 'api': [1, 2, 3, 6], + 'placement': 1, + 'minduration': 0, + 'maxduration': 30, + 'startdelay': 0, + 'skip': 1, + 'plcmt': 4 + } + }, + }; + + const check = { + w: 1024, + h: 768, + mimes: [ 'video/mp4' ], + playbackmethod: [2, 4, 6], + api: [1, 2, 3, 6], + protocols: [3, 4], + placement: 1, + minduration: 0, + maxduration: 30, + startdelay: 0, + skip: 1, + plcmt: 4 + } + + expect(spec.isBidRequestValid(NEW_VIDEO_BID)).to.equal(true); + let requests = spec.buildRequests([NEW_VIDEO_BID], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video.w).to.equal(check.w); + expect(request.imp[0].video.h).to.equal(check.h); + expect(request.imp[0].video.placement).to.equal(check.placement); + expect(request.imp[0].video.minduration).to.equal(check.minduration); + expect(request.imp[0].video.maxduration).to.equal(check.maxduration); + expect(request.imp[0].video.startdelay).to.equal(check.startdelay); + expect(request.imp[0].video.skip).to.equal(check.skip); + expect(request.imp[0].video.plcmt).to.equal(check.plcmt); + expect(request.imp[0].video.mimes).to.deep.have.same.members(check.mimes); + expect(request.imp[0].video.playbackmethod).to.deep.have.same.members(check.playbackmethod); + expect(request.imp[0].video.api).to.deep.have.same.members(check.api); + expect(request.imp[0].video.protocols).to.deep.have.same.members(check.protocols); + }); + } + + if (FEATURES.VIDEO) { + it('should use bidder video params if they are set', () => { + let bid = utils.deepClone(VIDEO_BID); + const check = { + api: [1, 2], + mimes: ['video/mp4', 'video/x-flv'], + playbackmethod: [3, 4], + protocols: [5, 6], + placement: 1, + plcmt: 1, + minduration: 0, + maxduration: 30, + startdelay: 0, + w: 640, + h: 480 + + }; + bid.mediaTypes.video = {...check}; + bid.mediaTypes.video.context = 'instream'; + bid.mediaTypes.video.playerSize = [[640, 480]]; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + const requests = spec.buildRequests([bid], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video).to.deep.include({...check}); + }); + } }); - it('should return an empty array to indicate no valid bids', function () { - const mockBidResult = { - requestId: '9cf19229-34f6-4d06-bc1d-0e44e8d616c8', - cpm: 10.0, - creativeId: '1', - width: 300, - height: 250, - netRevenue: true, - currency: 'USD', - ttl: 1800, - ad: '', - adType: 'banner' - }; - const testServerResponse = { - headers: [], - body: [mockBidResult] - }; - const bidResponses = spec.interpretResponse(testServerResponse, tempBidRequests); - expect(bidResponses).deep.equal([mockBidResult]); + }); + + describe('interpretResponse()', function () { + context('when mediaType is banner', function () { + it('should correctly interpret valid banner response', function () { + const bid = utils.deepClone(BANNER_BID); + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const response = utils.deepClone(BANNER_BID_RESPONSE); + + const bids = spec.interpretResponse({ body: response }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + expect(bids[0].mediaType).to.equal('banner'); + expect(bids[0].burl).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].burl); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].requestId).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].impid); + expect(bids[0].cpm).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].price); + expect(bids[0].width).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].w); + expect(bids[0].height).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].h); + expect(bids[0].ad).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].adm); + expect(bids[0].creativeId).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].crid); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://dummydomain.com'); + expect(bids[0].ttl).to.equal(500); + expect(bids[0].netRevenue).to.equal(true); + }); + + it('should handle empty bid response', function () { + const bid = utils.deepClone(BANNER_BID); + + let request = spec.buildRequests([bid], BIDDER_REQUEST)[0]; + const EMPTY_RESP = Object.assign({}, BANNER_BID_RESPONSE, { 'body': {} }); + const bids = spec.interpretResponse(EMPTY_RESP, request); + expect(bids).to.be.empty; + }); }); + if (FEATURES.VIDEO) { + context('when mediaType is video', function () { + it('should correctly interpret valid instream video response', () => { + const bid = utils.deepClone(VIDEO_BID); + + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const bids = spec.interpretResponse({ body: VIDEO_BID_RESPONSE }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].burl).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].burl); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].requestId).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].impid); + expect(bids[0].cpm).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].price); + expect(bids[0].width).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].w); + expect(bids[0].height).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].h); + expect(bids[0].vastXml).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].adm); + expect(bids[0].rendererUrl).to.equal(VIDEO_RENDERER_URL); + expect(bids[0].creativeId).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].crid); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://dummydomain.com'); + expect(bids[0].ttl).to.equal(500); + expect(bids[0].netRevenue).to.equal(true); + }); + }); + } }); describe('getUserSync', function () { diff --git a/test/spec/modules/trafficgateBidAdapter_spec.js b/test/spec/modules/trafficgateBidAdapter_spec.js index ecea0e69ca1..11ff547cc78 100644 --- a/test/spec/modules/trafficgateBidAdapter_spec.js +++ b/test/spec/modules/trafficgateBidAdapter_spec.js @@ -1,223 +1,1373 @@ -import { expect } from 'chai' -import { spec } from '../../../modules/trafficgateBidAdapter' -import { deepStrictEqual, notStrictEqual, ok, strictEqual } from 'assert' - -describe('TrafficGateAdapter', () => { - const bid = { - bidId: '9ec5b177515ee2e5', - bidder: 'trafficgate', +import {expect} from 'chai'; +import {spec} from 'modules/trafficgateBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from 'src/mediaTypes.js'; +import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import 'src/prebid.js' +import 'modules/currency.js'; +import 'modules/userId/index.js'; +import 'modules/multibid/index.js'; +import 'modules/priceFloors.js'; +import 'modules/consentManagement.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/schain.js'; +import {deepClone} from 'src/utils.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {hook} from '../../../src/hook.js'; + +const BidRequestBuilder = function BidRequestBuilder(options) { + const defaults = { + request: { + auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', + adUnitCode: 'adunit-code', + bidder: 'trafficgate' + }, params: { - placementId: 1, + placementId: '98765', host: 'example' }, - mediaTypes: { - banner: { - sizes: [[300, 250]] - } + sizes: [[300, 250], [300, 600]], + }; + + const request = { + ...defaults.request, + ...options + }; + + this.withParams = (options) => { + request.params = { + ...defaults.params, + ...options + }; + return this; + }; + + this.build = () => request; +}; + +const BidderRequestBuilder = function BidderRequestBuilder(options) { + const defaults = { + bidderCode: 'trafficgate', + auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', + bidderRequestId: '7g36s867Tr4xF90X', + timeout: 3000, + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'http://test.io/index.html?pbjs_debug=true' } - } - - describe('isBidRequestValid', () => { - it('Should return true if there are bidId, params and placementId parameters present', () => { - strictEqual(true, spec.isBidRequestValid(bid)) - }) - - it('Should return false if at least one of parameters is not present', () => { - const b = { ...bid } - delete b.params.placementId - strictEqual(false, spec.isBidRequestValid(b)) - }) - - it('Should return false if at least one of parameters is not present', () => { - const b = { ...bid } - delete b.params.host - strictEqual(false, spec.isBidRequestValid(b)) - }) - }) - - describe('buildRequests', () => { - const serverRequest = spec.buildRequests([bid]) - - it('Creates a ServerRequest object with method, URL and data', () => { - ok(serverRequest) - ok(serverRequest.method) - ok(serverRequest.url) - ok(serverRequest.data) - }) - - it('Returns POST method', () => { - strictEqual('POST', serverRequest.method) - }) - - it('Returns valid URL', () => { - strictEqual('https://example.bc-plugin.com/?c=o&m=multi', serverRequest.url) - }) - - it('Returns valid data if array of bids is valid', () => { - const { data } = serverRequest - strictEqual('object', typeof data) - deepStrictEqual(['language', 'secure', 'host', 'page', 'placements'], Object.keys(data)) - strictEqual('string', typeof data.language) - strictEqual('string', typeof data.host) - strictEqual('string', typeof data.page) - notStrictEqual(-1, [0, 1].indexOf(data.secure)) - - const placement = data.placements[0] - deepStrictEqual(['placementId', 'bidId', 'traffic'], Object.keys(placement)) - strictEqual(1, placement.placementId) - strictEqual('9ec5b177515ee2e5', placement.bidId) - strictEqual('banner', placement.traffic) - }) - - it('Returns empty data if no valid requests are passed', () => { - deepStrictEqual([], spec.buildRequests([])) - }) - }) - - describe('interpretResponse', () => { - const validData = [ - { - body: [{ - mediaType: 'banner', - width: 300, - height: 250, - cpm: 0.4, - ad: 'Test', - requestId: '9ec5b177515ee2e5', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1', - meta: { - advertiserDomains: ['test.com'] + }; + + const request = { + ...defaults, + ...options + }; + + this.build = () => request; +}; + +describe('TrafficgateOpenxRtbAdapter', function () { + before(() => { + hook.ready(); + }); + + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid()', function () { + describe('when request is for a banner ad', function () { + let bannerBid; + beforeEach(function () { + bannerBid = { + bidder: 'trafficgate', + params: {}, + adUnitCode: 'adunit-code', + mediaTypes: {banner: {}}, + sizes: [[300, 250], [300, 600]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475' + }; + }); + + it('should return false when there is placementId only', function () { + bannerBid.params = {'placementId': '98765'}; + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + describe('should return false when there is a host only', function () { + beforeEach(function () { + bannerBid.params = {host: 'test-delivery-domain'} + }); + + it('should return false when there is no placementId and size', function () { + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + it('should return false if there is an placementId without sizes', function () { + bannerBid.params.placementId = '98765'; + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + it('should return false if there is no placementId and sizes are defined', function () { + bannerBid.mediaTypes.banner.sizes = [720, 90]; + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + it('should return false if no sizes are defined ', function () { + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + it('should return false if sizes empty ', function () { + bannerBid.mediaTypes.banner.sizes = []; + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + it('should return true if there is placementId and sizes are defined', function () { + bannerBid.params.placementId = '98765'; + bannerBid.mediaTypes.banner.sizes = [720, 90]; + expect(spec.isBidRequestValid(bannerBid)).to.equal(true); + }); + }); + }); + + describe('when request is for a multiformat ad', function () { + describe('and request config uses mediaTypes video and banner', () => { + const multiformatBid = { + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250]] + }, + video: { + playerSize: [300, 250] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; + it('should return true multisize when required params found', function () { + expect(spec.isBidRequestValid(multiformatBid)).to.equal(true); + }); + }); + }); + + describe('when request is for a video ad', function () { + describe('and request config uses mediaTypes', () => { + const videoBidWithMediaTypes = { + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; + it('should return false when isBannerBid', function () { + expect(spec.isBannerBid(videoBidWithMediaTypes)).to.equal(false); + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(videoBidWithMediaTypes)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let videoBidWithMediaTypes = Object.assign({}, videoBidWithMediaTypes); + videoBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(videoBidWithMediaTypes)).to.equal(false); + }); + }); + describe('and request config uses both host and platform', () => { + const videoBidWithHostAndPlacement = { + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; + it('should return false when isBannerBid', function () { + expect(spec.isBannerBid(videoBidWithHostAndPlacement)).to.equal(false); + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(videoBidWithHostAndPlacement)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let videoBidWithMediaTypes = Object.assign({}, videoBidWithHostAndPlacement); + videoBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(videoBidWithMediaTypes)).to.equal(false); + }); + }); + describe('and request config uses mediaType', () => { + const videoBidWithMediaType = { + 'bidder': 'trafficgate', + 'params': { + 'placementId': '98765', + 'host': 'example' + }, + 'adUnitCode': 'adunit-code', + 'mediaType': 'video', + 'sizes': [640, 480], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(videoBidWithMediaType)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let videoBidWithMediaType = Object.assign({}, videoBidWithMediaType); + delete videoBidWithMediaType.params; + videoBidWithMediaType.params = {}; + expect(spec.isBidRequestValid(videoBidWithMediaType)).to.equal(false); + }); + }); + }); + }); + + describe('buildRequests()', function () { + let bidRequestsWithMediaTypes; + let bidRequestsWithPlatform; + let mockBidderRequest; + + beforeEach(function () { + mockBidderRequest = {refererInfo: {}}; + + bidRequestsWithMediaTypes = [{ + bidder: 'trafficgate', + params: { + placementId: '11', + host: 'example', + }, + adUnitCode: '/adunit-code/test-path', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] } - }] - }, - { - body: [{ - vastUrl: 'example.com', - mediaType: 'video', - cpm: 0.5, - requestId: '9ec5b177515ee2e5', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1', - meta: { - advertiserDomains: ['test.com'] + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + ortb2Imp: { + ext: { + ae: 2 } - }] - }, - { - body: [{ - mediaType: 'native', - clickUrl: 'example.com', - title: 'Test', - image: 'example.com', - creativeId: '2', - impressionTrackers: ['example.com'], - ttl: 120, - cpm: 0.4, - requestId: '9ec5b177515ee2e5', - netRevenue: true, - currency: 'USD', - meta: { - advertiserDomains: ['test.com'] + } + }, { + bidder: 'trafficgate', + params: { + placementId: '22', + host: 'example', + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480] } - }] - } - ] + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2' + }]; + }); - for (const obj of validData) { - const { mediaType } = obj.body[0] + context('common requests checks', function() { + it('should be able to handle multiformat requests', () => { + const multiformat = utils.deepClone(bidRequestsWithMediaTypes[0]); + multiformat.mediaTypes.video = { + context: 'outstream', + playerSize: [640, 480] + } + const requests = spec.buildRequests([multiformat], mockBidderRequest); + const outgoingFormats = requests.flatMap(rq => rq.data.imp.flatMap(imp => ['banner', 'video'].filter(k => imp[k] != null))); + const expected = FEATURES.VIDEO ? ['banner', 'video'] : ['banner'] + expect(outgoingFormats).to.have.members(expected); + }) - it(`Should interpret ${mediaType} response`, () => { - const response = spec.interpretResponse(obj) + it('should send bid request to trafficgate url via POST', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].url).to.equal('https://example.bc-plugin.com/prebidjs'); + expect(request[0].method).to.equal('POST'); + }); - expect(response).to.be.an('array') - strictEqual(1, response.length) + it('should send delivery domain, if available', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data.imp[0].ext.bidder.host).to.equal(bidRequestsWithMediaTypes[0].params.host); + expect(request[1].data.imp[0].ext.bidder.host).to.equal(bidRequestsWithMediaTypes[1].params.host); + }); - const copy = { ...obj.body[0] } - deepStrictEqual(copy, response[0]) - }) - } + it('should send placementId', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data.imp[0].ext.bidder.placementId).to.equal(bidRequestsWithMediaTypes[0].params.placementId); + expect(request[1].data.imp[0].ext.bidder.placementId).to.equal(bidRequestsWithMediaTypes[1].params.placementId); + }); + + describe('floors', function () { + it('should send out custom floors on bids that have customFloors, no currency as account currency is used', function () { + const bidRequest = Object.assign({}, + bidRequestsWithMediaTypes[0], + { + params: { + placementId: '98765', + host: 'example', + customFloor: 1.500 + } + } + ); - for (const obj of validData) { - it(`Should interpret response has meta.advertiserDomains`, () => { - const response = spec.interpretResponse(obj) + const request = spec.buildRequests([bidRequest], mockBidderRequest); + expect(request[0].data.imp[0].bidfloor).to.equal(bidRequest.params.customFloor); + expect(request[0].data.imp[0].bidfloorcur).to.equal(undefined); + }); - expect(response[0]['meta']['advertiserDomains']).to.be.an('array') - expect(response[0]['meta']['advertiserDomains'][0]).to.be.an('string') + context('with floors module', function () { + let adServerCurrencyStub; + + beforeEach(function () { + adServerCurrencyStub = sinon + .stub(config, 'getConfig') + .withArgs('currency.adServerCurrency') + }); + + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send out floors on bids in USD', function () { + const bidRequest = Object.assign({}, + bidRequestsWithMediaTypes[0], + { + getFloor: () => { + return { + currency: 'USD', + floor: 9.99 + } + } + } + ); + + const request = spec.buildRequests([bidRequest], mockBidderRequest); + expect(request[0].data.imp[0].bidfloor).to.equal(9.99); + expect(request[0].data.imp[0].bidfloorcur).to.equal('USD'); + }); + + it('should send not send floors', function () { + adServerCurrencyStub.returns('EUR'); + const bidRequest = Object.assign({}, + bidRequestsWithMediaTypes[0], + { + getFloor: () => { + return { + currency: 'BTC', + floor: 9.99 + } + } + } + ); + + const request = spec.buildRequests([bidRequest], mockBidderRequest); + expect(request[0].data.imp[0].bidfloor).to.equal(undefined) + expect(request[0].data.imp[0].bidfloorcur).to.equal(undefined) + }); + }) }) + + describe('FPD', function() { + let bidRequests; + const mockBidderRequest = {refererInfo: {}}; + + beforeEach(function () { + bidRequests = [{ + bidder: 'trafficgate', + params: { + placementId: '98765-banner', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-1' + }, { + bidder: 'trafficgate', + mediaTypes: { + video: { + playerSize: [640, 480] + } + }, + params: { + placementId: '98765-video', + host: 'example' + }, + 'adUnitCode': 'adunit-code', + + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-2' + }]; + }); + + it('ortb2.site should be merged in the request', function() { + const request = spec.buildRequests(bidRequests, { + ...mockBidderRequest, + 'ortb2': { + site: { + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'] + } + } + }); + let data = request[0].data; + expect(data.site.domain).to.equal('page.example.com'); + expect(data.site.cat).to.deep.equal(['IAB2']); + expect(data.site.sectioncat).to.deep.equal(['IAB2-2']); + }); + + it('ortb2.user should be merged in the request', function() { + const request = spec.buildRequests(bidRequests, { + ...mockBidderRequest, + 'ortb2': { + user: { + yob: 1985 + } + } + }); + let data = request[0].data; + expect(data.user.yob).to.equal(1985); + }); + + describe('ortb2Imp', function() { + describe('ortb2Imp.ext.data.pbadslot', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext).to.not.have.property('data'); + }); + + it('should not send if imp[].ext.data.pbadslot is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('pbadslot'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); + + it('should send if imp[].ext.data.pbadslot is string', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + pbadslot: 'abcd' + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext.data).to.have.property('pbadslot'); + expect(data.imp[0].ext.data.pbadslot).to.equal('abcd'); + }); + }); + + describe('ortb2Imp.ext.data.adserver', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext).to.not.have.property('data'); + }); + + it('should not send if imp[].ext.data.adserver is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('adserver'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); + + it('should send', function() { + let adSlotValue = 'abc'; + bidRequests[0].ortb2Imp = { + ext: { + data: { + adserver: { + name: 'GAM', + adslot: adSlotValue + } + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext.data.adserver.name).to.equal('GAM'); + expect(data.imp[0].ext.data.adserver.adslot).to.equal(adSlotValue); + }); + }); + + describe('ortb2Imp.ext.data.other', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext).to.not.have.property('data'); + }); + + it('should not send if imp[].ext.data.other is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('other'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); + + it('ortb2Imp.ext.data.other', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + other: 1234 + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext.data.other).to.equal(1234); + }); + }); + }); + + describe('with user agent client hints', function () { + it('should add device.sua if available', function () { + const bidderRequestWithUserAgentClientHints = { refererInfo: {}, + ortb2: { + device: { + sua: { + source: 2, + platform: { + brand: 'macOS', + version: [ '12', '4', '0' ] + }, + browsers: [ + { + brand: 'Chromium', + version: [ '106', '0', '5249', '119' ] + }, + { + brand: 'Google Chrome', + version: [ '106', '0', '5249', '119' ] + }, + { + brand: 'Not;A=Brand', + version: [ '99', '0', '0', '0' ] + }], + mobile: 0, + model: 'Pro', + bitness: '64', + architecture: 'x86' + } + } + }}; + + let request = spec.buildRequests(bidRequests, bidderRequestWithUserAgentClientHints); + expect(request[0].data.device.sua).to.exist; + expect(request[0].data.device.sua).to.deep.equal(bidderRequestWithUserAgentClientHints.ortb2.device.sua); + const bidderRequestWithoutUserAgentClientHints = {refererInfo: {}, ortb2: {}}; + request = spec.buildRequests(bidRequests, bidderRequestWithoutUserAgentClientHints); + expect(request[0].data.device?.sua).to.not.exist; + }); + }); + }); + + context('when there is a consent management framework', function () { + let bidRequests; + let mockConfig; + let bidderRequest; + + beforeEach(function () { + bidRequests = [{ + bidder: 'trafficgate', + params: { + placementId: '98765-banner', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-1' + }, { + bidder: 'trafficgate', + mediaTypes: { + video: { + playerSize: [640, 480] + } + }, + params: { + placementId: '98765-video', + host: 'example' + }, + 'adUnitCode': 'adunit-code', + + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-2' + }]; + }); + + describe('us_privacy', function () { + beforeEach(function () { + bidderRequest = { + uspConsent: '1YYN', + refererInfo: {} + }; + + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); + }); + + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send a signal to specify that US Privacy applies to this request', function () { + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs.ext.us_privacy).to.equal('1YYN'); + expect(request[1].data.regs.ext.us_privacy).to.equal('1YYN'); + }); + + it('should not send the regs object, when consent string is undefined', function () { + delete bidderRequest.uspConsent; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs?.us_privacy).to.not.exist; + }); + }); + + describe('GDPR', function () { + beforeEach(function () { + bidderRequest = { + gdprConsent: { + consentString: 'test-gdpr-consent-string', + addtlConsent: 'test-addtl-consent-string', + gdprApplies: true + }, + refererInfo: {} + }; + + mockConfig = { + consentManagement: { + cmpApi: 'iab', + timeout: 1111, + allowAuctionWithoutConsent: 'cancel' + } + }; + + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); + }); + + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send a signal to specify that GDPR applies to this request', function () { + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs.ext.gdpr).to.equal(1); + expect(request[1].data.regs.ext.gdpr).to.equal(1); + }); + + it('should send the consent string', function () { + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + expect(request[1].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + }); + + it('should send the addtlConsent string', function () { + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.user.ext.ConsentedProvidersSettings.consented_providers).to.equal(bidderRequest.gdprConsent.addtlConsent); + expect(request[1].data.user.ext.ConsentedProvidersSettings.consented_providers).to.equal(bidderRequest.gdprConsent.addtlConsent); + }); + + it('should send a signal to specify that GDPR does not apply to this request', function () { + bidderRequest.gdprConsent.gdprApplies = false; + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs.ext.gdpr).to.equal(0); + expect(request[1].data.regs.ext.gdpr).to.equal(0); + }); + + it('when GDPR application is undefined, should not send a signal to specify whether GDPR applies to this request, ' + + 'but can send consent data, ', function () { + delete bidderRequest.gdprConsent.gdprApplies; + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs?.ext?.gdpr).to.not.be.ok; + expect(request[0].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + expect(request[1].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + }); + + it('when consent string is undefined, should not send the consent string, ', function () { + delete bidderRequest.gdprConsent.consentString; + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.imp[0].ext.consent).to.equal(undefined); + expect(request[1].data.imp[0].ext.consent).to.equal(undefined); + }); + }); + }); + + context('coppa', function() { + it('when there are no coppa param settings, should not send a coppa flag', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.regs?.coppa).to.be.not.ok; + }); + + it('should send a coppa flag there is when there is coppa param settings in the bid requests', function () { + let mockConfig = { + coppa: true + }; + + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); + + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.regs.coppa).to.equal(1); + }); + + it('should send a coppa flag there is when there is coppa param settings in the bid params', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + request.params = {coppa: true}; + expect(request[0].data.regs.coppa).to.equal(1); + }); + + after(function () { + config.getConfig.restore() + }); + }); + + context('do not track (DNT)', function() { + let doNotTrackStub; + + beforeEach(function () { + doNotTrackStub = sinon.stub(utils, 'getDNT'); + }); + afterEach(function() { + doNotTrackStub.restore(); + }); + + it('when there is a do not track, should send a dnt', function () { + doNotTrackStub.returns(1); + + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.device.dnt).to.equal(1); + }); + + it('when there is not do not track, don\'t send dnt', function () { + doNotTrackStub.returns(0); + + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.device.dnt).to.equal(0); + }); + + it('when there is no defined do not track, don\'t send dnt', function () { + doNotTrackStub.returns(null); + + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.device.dnt).to.equal(0); + }); + }); + + context('supply chain (schain)', function () { + let bidRequests; + let schainConfig; + const supplyChainNodePropertyOrder = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + + beforeEach(function () { + schainConfig = { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'exchange1.com', + sid: '1234', + hp: 1, + rid: 'bid-request-1', + name: 'publisher', + domain: 'publisher.com' + // omitted ext + }, + { + asi: 'exchange2.com', + sid: 'abcd', + hp: 1, + rid: 'bid-request-2', + // name field missing + domain: 'intermediary.com' + }, + { + asi: 'exchange3.com', + sid: '4321', + hp: 1, + // request id + // name field missing + domain: 'intermediary-2.com' + } + ] + }; + + bidRequests = [{ + bidder: 'trafficgate', + params: { + placementId: '11', + host: 'example' + }, + adUnitCode: '/adunit-code/test-path', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + schain: schainConfig + }]; + }); + + it('should send a supply chain object', function () { + const request = spec.buildRequests(bidRequests, mockBidderRequest); + expect(request[0].data.source.ext.schain).to.equal(schainConfig); + }); + + it('should send the supply chain object with the right version', function () { + const request = spec.buildRequests(bidRequests, mockBidderRequest); + expect(request[0].data.source.ext.schain.ver).to.equal(schainConfig.ver); + }); + + it('should send the supply chain object with the right complete value', function () { + const request = spec.buildRequests(bidRequests, mockBidderRequest); + expect(request[0].data.source.ext.schain.complete).to.equal(schainConfig.complete); + }); + }); + + context('when there are userid providers', function () { + const userIdAsEids = [ + { + source: 'adserver.org', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + rtiPartner: 'TDID' + } + }] + }, + { + source: 'id5-sync.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { + source: 'sharedid.org', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + third: 'some-random-id-value' + } + }] + } + ]; + + it(`should send the user id under the extended ids`, function () { + const bidRequestsWithUserId = [{ + bidder: 'trafficgate', + params: { + placementId: '11', + host: 'example' + }, + userId: { + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + userIdAsEids: userIdAsEids + }]; + // enrich bid request with userId key/value + + const request = spec.buildRequests(bidRequestsWithUserId, mockBidderRequest); + expect(request[0].data.user.ext.eids).to.equal(userIdAsEids); + }); + + it(`when no user ids are available, it should not send any extended ids`, function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data).to.not.have.any.keys('user'); + }); + }); + + context('FLEDGE', function() { + it('when FLEDGE is enabled, should send whatever is set in ortb2imp.ext.ae in all bid requests', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, { + ...mockBidderRequest, + fledgeEnabled: true + }); + expect(request[0].data.imp[0].ext.ae).to.equal(2); + }); + }); + }); + + context('banner', function () { + it('should send bid request with a mediaTypes specified with banner type', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data.imp[0]).to.have.any.keys(BANNER); + }); + }); + + if (FEATURES.VIDEO) { + context('video', function () { + it('should send bid request with a mediaTypes specified with video type', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[1].data.imp[0]).to.have.any.keys(VIDEO); + }); + + it('Update imp.video with OpenRTB options from mimeTypes and params', function() { + const bid01 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-01', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + mimes: ['video/mp4'], + protocols: [8] + } + }, + }).withParams({ + // options in video, will merge + video: { + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30 + } + }).build(); + + const bidderRequest = new BidderRequestBuilder().build(); + const expected = { + mimes: ['video/mp4'], + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30, + placement: 4, + protocols: [8], + w: 300, + h: 250 + }; + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests).to.have.lengthOf(2); + expect(requests[1].data.imp[0].video).to.deep.equal(expected); + }); + }); } + }); - const invalidData = [ - { - body: [{ - width: 300, - cpm: 0.4, - ad: 'Test', - requestId: '9ec5b177515ee2e5', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] + describe('interpretResponse()', function () { + let bidRequestConfigs; + let bidRequest; + let bidResponse; + let bid; + + context('when there is an nbr response', function () { + let bids; + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = {nbr: 0}; // Unknown error + bids = spec.interpretResponse({body: bidResponse}, bidRequest); + }); + + it('should not return any bids', function () { + expect(bids.length).to.equal(0); + }); + }); + + context('when no seatbid in response', function () { + let bids; + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = {ext: {}, id: 'test-bid-id'}; + bids = spec.interpretResponse({body: bidResponse}, bidRequest); + }); + + it('should not return any bids', function () { + expect(bids.length).to.equal(0); + }); + }); + + context('when there is no response', function () { + let bids; + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = ''; // Unknown error + bids = spec.interpretResponse({body: bidResponse}, bidRequest); + }); + + it('should not return any bids', function () { + expect(bids.length).to.equal(0); + }); + }); + + const SAMPLE_BID_REQUESTS = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' }, - { - body: [{ - mediaType: 'video', - cpm: 0.5, - requestId: '9ec5b177515ee2e5', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, }, - { - body: [{ - mediaType: 'native', - clickUrl: 'example.com', - title: 'Test', - impressionTrackers: ['example.com'], - ttl: 120, - requestId: '9ec5b177515ee2e5', - creativeId: '2', - netRevenue: true, - currency: 'USD', + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + const SAMPLE_BID_RESPONSE = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 3.5, + w: 300, + h: 250, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup', + adomain: ['brand.com'], + ext: { + networkId: 123, + advertiserDomains: ['domain.com'], + } }] - } - ] + }], + cur: 'USD' + }; - for (const obj of invalidData) { - const { mediaType } = obj.body[0] + context('when there is a response, the common response properties', function () { + beforeEach(function () { + bidRequestConfigs = deepClone(SAMPLE_BID_REQUESTS); + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + bidResponse = deepClone(SAMPLE_BID_RESPONSE); - it(`Should return an empty array if invalid ${mediaType} response is passed `, () => { - const response = spec.interpretResponse(obj) + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + }); - expect(response).to.be.an('array') - strictEqual(0, response.length) - }) - } + it('should return a price', function () { + expect(bid.cpm).to.equal(bidResponse.seatbid[0].bid[0].price); + }); - it('Should return an empty array if invalid response is passed', () => { - const response = spec.interpretResponse({ - body: [{ - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] - }) + it('should return a request id', function () { + expect(bid.requestId).to.equal(bidResponse.seatbid[0].bid[0].impid); + }); + + it('should return width and height for the creative', function () { + expect(bid.width).to.equal(bidResponse.seatbid[0].bid[0].w); + expect(bid.height).to.equal(bidResponse.seatbid[0].bid[0].h); + }); - expect(response).to.be.an('array') - strictEqual(0, response.length) - }) - }) -}) + it('should return a creativeId', function () { + expect(bid.creativeId).to.equal(bidResponse.seatbid[0].bid[0].crid); + }); + + it('should return an ad', function () { + expect(bid.ad).to.equal(bidResponse.seatbid[0].bid[0].adm); + }); + + it('should return a deal id if it exists', function () { + expect(bid.dealId).to.equal(bidResponse.seatbid[0].bid[0].dealid); + }); + + it('should have a time-to-live of 5 minutes', function () { + expect(bid.ttl).to.equal(300); + }); + + it('should always return net revenue', function () { + expect(bid.netRevenue).to.equal(true); + }); + + it('should return a currency', function () { + expect(bid.currency).to.equal(bidResponse.cur); + }); + + it('should return a networkId', function () { + expect(bid.meta.networkId).to.equal(bidResponse.seatbid[0].bid[0].ext.networkId); + }); + + it('should return adomain', function () { + expect(bid.meta.advertiserDomains).to.equal(bidResponse.seatbid[0].bid[0].ext.advertiserDomains); + }); + }); + + context('when the response is a banner', function() { + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 2, + w: 300, + h: 250, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup' + }] + }], + cur: 'AUS' + }; + + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + }); + + it('should return the proper mediaType', function () { + it('should return a creativeId', function () { + expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); + }); + }); + }); + + if (FEATURES.VIDEO) { + context('when the response is a video', function() { + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [[640, 360], [854, 480]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 2, + w: 854, + h: 480, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup', + }] + }], + cur: 'AUS' + }; + }); + + it('should return the proper mediaType', function () { + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); + }); + + it('should return the proper mediaType', function () { + const winUrl = 'https//my.win.url'; + bidResponse.seatbid[0].bid[0].nurl = winUrl + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + + expect(bid.vastUrl).to.equal(winUrl); + }); + }); + } + }); +}); diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index 718d030be91..275b9b3bfee 100644 --- a/test/spec/modules/tripleliftBidAdapter_spec.js +++ b/test/spec/modules/tripleliftBidAdapter_spec.js @@ -8,6 +8,7 @@ import * as utils from 'src/utils.js'; const ENDPOINT = 'https://tlx.3lift.com/header/auction?'; const GDPR_CONSENT_STR = 'BOONm0NOONm0NABABAENAa-AAAARh7______b9_3__7_9uz_Kv_K7Vf7nnG072lPVA9LTOQ6gEaY'; +const GPP_CONSENT_STR = 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN' describe('triplelift adapter', function () { const adapter = newBidder(tripleliftAdapterSpec); @@ -1039,6 +1040,12 @@ describe('triplelift adapter', function () { const url = request.url; expect(url).to.match(/(\?|&)us_privacy=1YYY/); }); + it('should pass fledge signal when Triplelift is eligible for fledge', function() { + bidderRequest.fledgeEnabled = true; + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + const url = request.url; + expect(url).to.match(/(\?|&)fledge=true/); + }); it('should return coppa param when COPPA config is set to true', function() { sinon.stub(config, 'getConfig').withArgs('coppa').returns(true); const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); @@ -1123,6 +1130,46 @@ describe('triplelift adapter', function () { expect(logErrorSpy.calledOnce).to.equal(true); }); + it('should add ortb2 ext object if global fpd is available', function() { + const ortb2 = { + site: { + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + }, + user: { + yob: 1985, + gender: 'm', + keywords: 'a,b', + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 4 }, + segment: [{ id: '1' }] + } + ], + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + } + }; + + const request = tripleliftAdapterSpec.buildRequests(bidRequests, {...bidderRequest, ortb2}); + const { data: payload } = request; + expect(payload.ext.ortb2).to.exist; + expect(payload.ext.ortb2.site).to.deep.equal({ + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + }); + }); it('should send global config fpd if kvps are available', function() { const sens = null; const category = ['news', 'weather', 'hurricane']; @@ -1501,11 +1548,63 @@ describe('triplelift adapter', function () { expect(result[2].meta.networkId).to.equal('5989'); expect(result[3].meta.networkId).to.equal('5989'); }); + + it('should return fledgeAuctionConfigs if PAAPI response is received', function() { + response.body.paapi = [ + { + imp_id: '0', + auctionConfig: { + seller: 'https://3lift.com', + decisionLogicUrl: 'https://3lift.com/decision_logic.js', + interestGroupBuyers: ['https://some_buyer.com'], + perBuyerSignals: { + 'https://some_buyer.com': { a: 1 } + } + } + }, + { + imp_id: '2', + auctionConfig: { + seller: 'https://3lift.com', + decisionLogicUrl: 'https://3lift.com/decision_logic.js', + interestGroupBuyers: ['https://some_other_buyer.com'], + perBuyerSignals: { + 'https://some_other_buyer.com': { b: 2 } + } + } + } + ]; + + let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); + + expect(result).to.have.property('bids'); + expect(result).to.have.property('fledgeAuctionConfigs'); + expect(result.fledgeAuctionConfigs.length).to.equal(2); + expect(result.fledgeAuctionConfigs[0].bidId).to.equal('30b31c1838de1e'); + expect(result.fledgeAuctionConfigs[1].bidId).to.equal('73edc0ba8de203'); + expect(result.fledgeAuctionConfigs[0].config).to.deep.equal( + { + 'seller': 'https://3lift.com', + 'decisionLogicUrl': 'https://3lift.com/decision_logic.js', + 'interestGroupBuyers': ['https://some_buyer.com'], + 'perBuyerSignals': { 'https://some_buyer.com': { 'a': 1 } } + } + ); + expect(result.fledgeAuctionConfigs[1].config).to.deep.equal( + { + 'seller': 'https://3lift.com', + 'decisionLogicUrl': 'https://3lift.com/decision_logic.js', + 'interestGroupBuyers': ['https://some_other_buyer.com'], + 'perBuyerSignals': { 'https://some_other_buyer.com': { 'b': 2 } } + } + ); + }); }); describe('getUserSyncs', function() { let expectedIframeSyncUrl = 'https://eb2.3lift.com/sync?gdpr=true&cmp_cs=' + GDPR_CONSENT_STR + '&'; let expectedImageSyncUrl = 'https://eb2.3lift.com/sync?px=1&src=prebid&gdpr=true&cmp_cs=' + GDPR_CONSENT_STR + '&'; + let expectedGppSyncUrl = 'https://eb2.3lift.com/sync?gdpr=true&cmp_cs=' + GDPR_CONSENT_STR + '&gpp=' + GPP_CONSENT_STR + '&gpp_sid=2%2C8' + '&'; it('returns undefined when syncing is not enabled', function() { expect(tripleliftAdapterSpec.getUserSyncs({})).to.equal(undefined); @@ -1543,8 +1642,19 @@ describe('triplelift adapter', function () { let syncOptions = { iframeEnabled: true }; - let result = tripleliftAdapterSpec.getUserSyncs(syncOptions, null, null, '1YYY'); + let result = tripleliftAdapterSpec.getUserSyncs(syncOptions, null, null, '1YYY', null); expect(result[0].url).to.match(/(\?|&)us_privacy=1YYY/); }); + it('returns a user sync pixel with GPP signals when available', function() { + let syncOptions = { + iframeEnabled: true + }; + let gppConsent = { + 'applicableSections': [2, 8], + 'gppString': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN' + } + let result = tripleliftAdapterSpec.getUserSyncs(syncOptions, null, null, null, gppConsent); + expect(result[0].url).to.equal(expectedGppSyncUrl); + }); }); }); diff --git a/test/spec/modules/ttdBidAdapter_spec.js b/test/spec/modules/ttdBidAdapter_spec.js index 56c506dea6b..1fe504ba8e8 100644 --- a/test/spec/modules/ttdBidAdapter_spec.js +++ b/test/spec/modules/ttdBidAdapter_spec.js @@ -262,6 +262,11 @@ describe('ttdBidAdapter', function () { expect(request.data).to.be.not.null; }); + it('sets bidrequest.id to bidderRequestId', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.id).to.equal('18084284054531'); + }); + it('sets impression id to ad unit\'s bid id', function () { const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; expect(requestBody.imp[0].id).to.equal('243310435309b5'); diff --git a/test/spec/modules/ucfunnelBidAdapter_spec.js b/test/spec/modules/ucfunnelBidAdapter_spec.js index 992708bd2ea..9bec7229450 100644 --- a/test/spec/modules/ucfunnelBidAdapter_spec.js +++ b/test/spec/modules/ucfunnelBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/ucfunnelBidAdapter.js'; import {BANNER, VIDEO, NATIVE} from 'src/mediaTypes.js'; +import {deepClone} from '../../../src/utils.js'; const URL = 'https://hb.aralego.com/header'; const BIDDER_CODE = 'ucfunnel'; @@ -150,7 +151,10 @@ describe('ucfunnel Adapter', function () { }); }); describe('build request', function () { - const request = spec.buildRequests([validBannerBidReq], bidderRequest); + let request; + before(() => { + request = spec.buildRequests([validBannerBidReq], bidderRequest); + }) it('should create a POST request for every bid', function () { expect(request[0].method).to.equal('GET'); expect(request[0].url).to.equal(spec.ENDPOINT); @@ -179,15 +183,16 @@ describe('ucfunnel Adapter', function () { it('must parse bid size from a nested array', function () { const width = 640; const height = 480; - validBannerBidReq.sizes = [[ width, height ]]; - const requests = spec.buildRequests([ validBannerBidReq ], bidderRequest); + const bid = deepClone(validBannerBidReq); + bid.sizes = [[ width, height ]]; + const requests = spec.buildRequests([ bid ], bidderRequest); const data = requests[0].data; expect(data.w).to.equal(width); expect(data.h).to.equal(height); }); it('should set bidfloor if configured', function() { - let bid = Object.assign({}, validBannerBidReq); + let bid = deepClone(validBannerBidReq); bid.getFloor = function() { return { currency: 'USD', @@ -200,7 +205,7 @@ describe('ucfunnel Adapter', function () { }); it('should set bidfloor if configured', function() { - let bid = Object.assign({}, validBannerBidReq); + let bid = deepClone(validBannerBidReq); bid.params.bidfloor = 2.01; const requests = spec.buildRequests([ bid ], bidderRequest); const data = requests[0].data; @@ -208,7 +213,7 @@ describe('ucfunnel Adapter', function () { }); it('should set bidfloor if configured', function() { - let bid = Object.assign({}, validBannerBidReq); + let bid = deepClone(validBannerBidReq); bid.getFloor = function() { return { currency: 'USD', @@ -224,8 +229,12 @@ describe('ucfunnel Adapter', function () { describe('interpretResponse', function () { describe('should support banner', function () { - const request = spec.buildRequests([ validBannerBidReq ], bidderRequest); - const result = spec.interpretResponse({body: validBannerBidRes}, request[0]); + let request, result; + before(() => { + request = spec.buildRequests([ validBannerBidReq ], bidderRequest); + result = spec.interpretResponse({body: validBannerBidRes}, request[0]); + }); + it('should build bid array for banner', function () { expect(result.length).to.equal(1); }); @@ -243,8 +252,11 @@ describe('ucfunnel Adapter', function () { }); describe('handle banner no ad', function () { - const request = spec.buildRequests([ validBannerBidReq ], bidderRequest); - const result = spec.interpretResponse({body: invalidBannerBidRes}, request[0]); + let request, result; + before(() => { + request = spec.buildRequests([ validBannerBidReq ], bidderRequest); + result = spec.interpretResponse({body: invalidBannerBidRes}, request[0]); + }) it('should build bid array for banner', function () { expect(result.length).to.equal(1); }); @@ -261,8 +273,11 @@ describe('ucfunnel Adapter', function () { }); describe('handle banner cpm under bidfloor', function () { - const request = spec.buildRequests([ validBannerBidReq ], bidderRequest); - const result = spec.interpretResponse({body: invalidBannerBidRes}, request[0]); + let request, result; + before(() => { + request = spec.buildRequests([ validBannerBidReq ], bidderRequest); + result = spec.interpretResponse({body: invalidBannerBidRes}, request[0]); + }) it('should build bid array for banner', function () { expect(result.length).to.equal(1); }); @@ -279,8 +294,11 @@ describe('ucfunnel Adapter', function () { }); describe('should support video', function () { - const request = spec.buildRequests([ validVideoBidReq ], bidderRequest); - const result = spec.interpretResponse({body: validVideoBidRes}, request[0]); + let request, result; + before(() => { + request = spec.buildRequests([ validVideoBidReq ], bidderRequest); + result = spec.interpretResponse({body: validVideoBidRes}, request[0]); + }) it('should build bid array', function () { expect(result.length).to.equal(1); }); @@ -299,8 +317,11 @@ describe('ucfunnel Adapter', function () { }); describe('should support native', function () { - const request = spec.buildRequests([ validNativeBidReq ], bidderRequest); - const result = spec.interpretResponse({body: validNativeBidRes}, request[0]); + let request, result; + before(() => { + request = spec.buildRequests([ validNativeBidReq ], bidderRequest); + result = spec.interpretResponse({body: validNativeBidRes}, request[0]); + }) it('should build bid array', function () { expect(result.length).to.equal(1); }); diff --git a/test/spec/modules/uid2IdSystem_helpers.js b/test/spec/modules/uid2IdSystem_helpers.js index 65d52c1d7c3..e0bef047acb 100644 --- a/test/spec/modules/uid2IdSystem_helpers.js +++ b/test/spec/modules/uid2IdSystem_helpers.js @@ -1,6 +1,6 @@ -import { setConsentConfig } from 'modules/consentManagement.js'; -import { server } from 'test/mocks/xhr.js'; -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {setConsentConfig} from 'modules/consentManagement.js'; +import {server} from 'test/mocks/xhr.js'; +import {coreStorage, requestBidsHook} from 'modules/userId/index.js'; const msIn12Hours = 60 * 60 * 12 * 1000; const expireCookieDate = 'Thu, 01 Jan 1970 00:00:01 GMT'; @@ -26,16 +26,16 @@ export const runAuction = async () => { } export const apiHelpers = { - makeTokenResponse: (token, shouldRefresh = false, expired = false) => ({ + makeTokenResponse: (token, shouldRefresh = false, expired = false, refreshExpired = false) => ({ advertising_token: token, refresh_token: 'fake-refresh-token', identity_expires: expired ? Date.now() - 1000 : Date.now() + 60 * 60 * 1000, refresh_from: shouldRefresh ? Date.now() - 1000 : Date.now() + 60 * 1000, - refresh_expires: Date.now() + 24 * 60 * 60 * 1000, // 24 hours + refresh_expires: refreshExpired ? Date.now() - 1000 : Date.now() + 24 * 60 * 60 * 1000, // 24 hours refresh_response_key: 'wR5t6HKMfJ2r4J7fEGX9Gw==', // Fake data }), - respondAfterDelay: (delay) => new Promise((resolve) => setTimeout(() => { - server.respond(); + respondAfterDelay: (delay, srv = server) => new Promise((resolve) => setTimeout(() => { + srv.respond(); setTimeout(() => resolve()); }, delay)), } diff --git a/test/spec/modules/uid2IdSystem_spec.js b/test/spec/modules/uid2IdSystem_spec.js index 20a38a292bb..8e3728704c7 100644 --- a/test/spec/modules/uid2IdSystem_spec.js +++ b/test/spec/modules/uid2IdSystem_spec.js @@ -1,17 +1,17 @@ /* eslint-disable no-console */ -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {coreStorage, init, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import { uid2IdSubmodule } from 'modules/uid2IdSystem.js'; import 'src/prebid.js'; import 'modules/consentManagement.js'; import { getGlobal } from 'src/prebidGlobal.js'; -import { server } from 'test/mocks/xhr.js'; import { configureTimerInterceptors } from 'test/mocks/timers.js'; import { cookieHelpers, runAuction, apiHelpers, setGdprApplies } from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; +import {server} from 'test/mocks/xhr'; let expect = require('chai').expect; @@ -24,16 +24,22 @@ const auctionDelayMs = 10; const initialToken = `initial-advertising-token`; const legacyToken = 'legacy-advertising-token'; const refreshedToken = 'refreshed-advertising-token'; +const clientSideGeneratedToken = 'client-side-generated-advertising-token'; const legacyConfigParams = {storage: null}; const serverCookieConfigParams = { uid2ServerCookie: publisherCookieName }; const newServerCookieConfigParams = { uid2Cookie: publisherCookieName }; +const cstgConfigParams = { serverPublicKey: 'UID2-X-L-24B8a/eLYBmRkXA9yPgRZt+ouKbXewG2OPs23+ov3JC8mtYJBCx6AxGwJ4MlwUcguebhdDp2CvzsCgS9ogwwGA==', subscriptionId: 'subscription-id' } const makeUid2IdentityContainer = (token) => ({uid2: {id: token}}); let useLocalStorage = false; const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ({ userSync: { auctionDelay: auctionDelayMs, userIds: [{name: 'uid2', params: {storage: useLocalStorage ? 'localStorage' : 'cookie', ...params}}] }, debug, ...extraSettings }); +const makeOriginalIdentity = (identity, salt = 1) => ({ + identity: utils.cyrb53Hash(identity, salt), + salt +}) const getFromAppropriateStorage = () => { if (useLocalStorage) return coreStorage.getDataFromLocalStorage(moduleCookieName); @@ -47,31 +53,18 @@ const expectGlobalToHaveToken = (token) => expect(getGlobal().getUserIds()).to.d const expectGlobalToHaveNoUid2 = () => expect(getGlobal().getUserIds()).to.not.haveOwnProperty('uid2'); const expectNoLegacyToken = (bid) => expect(bid.userId).to.not.deep.include(makeUid2IdentityContainer(legacyToken)); const expectModuleStorageEmptyOrMissing = () => expect(getFromAppropriateStorage()).to.be.null; -const expectModuleStorageToContain = (initialIdentity, latestIdentity) => { +const expectModuleStorageToContain = (originalAdvertisingToken, latestAdvertisingToken, originalIdentity) => { const cookie = JSON.parse(getFromAppropriateStorage()); - if (initialIdentity) expect(cookie.originalToken.advertising_token).to.equal(initialIdentity); - if (latestIdentity) expect(cookie.latestToken.advertising_token).to.equal(latestIdentity); + if (originalAdvertisingToken) expect(cookie.originalToken.advertising_token).to.equal(originalAdvertisingToken); + if (latestAdvertisingToken) expect(cookie.latestToken.advertising_token).to.equal(latestAdvertisingToken); + if (originalIdentity) expect(cookie.originalIdentity).to.eql(makeOriginalIdentity(Object.values(originalIdentity)[0], cookie.originalIdentity.salt)); } -const apiUrl = 'https://prod.uidapi.com/v2/token/refresh'; +const apiUrl = 'https://prod.uidapi.com/v2/token' +const refreshApiUrl = `${apiUrl}/refresh`; const headers = { 'Content-Type': 'application/json' }; -const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); -const configureUid2Response = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); -const configureUid2ApiSuccessResponse = () => configureUid2Response(200, makeSuccessResponseBody()); -const configureUid2ApiFailResponse = () => configureUid2Response(500, 'Error'); - -// Runs the provided test twice - once with a successful API mock, once with one which returns a server error -const testApiSuccessAndFailure = (act, testDescription, failTestDescription, only = false) => { - const testFn = only ? it.only : it; - testFn(`API responds successfully: ${testDescription}`, async function() { - configureUid2ApiSuccessResponse(); - await act(true); - }); - testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { - configureUid2ApiFailResponse(); - await act(false); - }); -} +const makeSuccessResponseBody = (responseToken) => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: responseToken } })); +const cstgApiUrl = `${apiUrl}/client-generate`; const testCookieAndLocalStorage = (description, test, only = false) => { const describeFn = only ? describe.only : describe; @@ -104,10 +97,18 @@ describe(`UID2 module`, function () { // I've confirmed it's available in Firefox since v34 (it seems to be unavailable on BrowserStack in Firefox v106). if (typeof window.crypto.subtle === 'undefined') { restoreSubtleToUndefined = true; - window.crypto.subtle = { importKey: () => {}, decrypt: () => {} }; + window.crypto.subtle = { importKey: () => {}, digest: () => {}, decrypt: () => {}, deriveKey: () => {}, encrypt: () => {}, generateKey: () => {}, exportKey: () => {} }; } suiteSandbox.stub(window.crypto.subtle, 'importKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'digest').callsFake(() => Promise.resolve('hashed_value')); suiteSandbox.stub(window.crypto.subtle, 'decrypt').callsFake((settings, key, data) => Promise.resolve(new Uint8Array([...settings.iv, ...data]))); + suiteSandbox.stub(window.crypto.subtle, 'deriveKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'exportKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'encrypt').callsFake(() => Promise.resolve(new ArrayBuffer())); + suiteSandbox.stub(window.crypto.subtle, 'generateKey').callsFake(() => Promise.resolve({ + privateKey: {}, + publicKey: {} + })); }); after(function () { @@ -116,14 +117,30 @@ describe(`UID2 module`, function () { if (restoreSubtleToUndefined) window.crypto.subtle = undefined; }); + const configureUid2Response = (apiUrl, httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + const configureUid2ApiSuccessResponse = (apiUrl, responseToken) => configureUid2Response(apiUrl, 200, makeSuccessResponseBody(responseToken)); + const configureUid2ApiFailResponse = (apiUrl) => configureUid2Response(apiUrl, 500, 'Error'); + // Runs the provided test twice - once with a successful API mock, once with one which returns a server error + const testApiSuccessAndFailure = (act, apiUrl, testDescription, failTestDescription, only = false, responseToken = refreshedToken) => { + const testFn = only ? it.only : it; + testFn(`API responds successfully: ${testDescription}`, async function() { + configureUid2ApiSuccessResponse(apiUrl, responseToken); + await act(true); + }); + testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { + configureUid2ApiFailResponse(apiUrl); + await act(false); + }); + } + const getFullTestTitle = (test) => `${test.parent.title ? getFullTestTitle(test.parent) + ' | ' : ''}${test.title}`; + beforeEach(function () { debugOutput(`----------------- START TEST ------------------`); fullTestTitle = getFullTestTitle(this.test.ctx.currentTest); debugOutput(fullTestTitle); testSandbox = sinon.sandbox.create(); testSandbox.stub(utils, 'logWarn'); - init(config); setSubmoduleRegistry([uid2IdSubmodule]); }); @@ -143,7 +160,6 @@ describe(`UID2 module`, function () { } cookieHelpers.clearCookies(moduleCookieName, publisherCookieName); coreStorage.removeDataFromLocalStorage(moduleCookieName); - debugOutput('----------------- END TEST ------------------'); }); @@ -151,13 +167,13 @@ describe(`UID2 module`, function () { it('When no baseUrl is provided in config, the module calls the production endpoint', function() { const uid2Token = apiHelpers.makeTokenResponse(initialToken, true, true); config.setConfig(makePrebidConfig({uid2Token})); - expect(server.requests[0]?.url).to.have.string('https://prod.uidapi.com/'); + expect(server.requests[0]?.url).to.have.string('https://prod.uidapi.com/v2/token/refresh'); }); it('When a baseUrl is provided in config, the module calls the provided endpoint', function() { const uid2Token = apiHelpers.makeTokenResponse(initialToken, true, true); config.setConfig(makePrebidConfig({uid2Token, uid2ApiBase: 'https://operator-integ.uidapi.com'})); - expect(server.requests[0]?.url).to.have.string('https://operator-integ.uidapi.com/'); + expect(server.requests[0]?.url).to.have.string('https://operator-integ.uidapi.com/v2/token/refresh'); }); }); @@ -238,7 +254,7 @@ describe(`UID2 module`, function () { cookieHelpers.setPublisherCookie(publisherCookieName, token); config.setConfig(makePrebidConfig(serverCookieConfigParams, extraConfig)); }, - } + }, ] scenarios.forEach(function(scenario) { @@ -247,16 +263,16 @@ describe(`UID2 module`, function () { describe('When the refresh is available in time', function() { testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - apiHelpers.respondAfterDelay(auctionDelayMs / 10); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); const bid = await runAuction(); if (apiSucceeds) expectToken(bid, refreshedToken); else expectNoIdentity(bid); - }, 'it should be used in the auction', 'the auction should have no uid2'); + }, refreshApiUrl, 'it should be used in the auction', 'the auction should have no uid2'); testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - apiHelpers.respondAfterDelay(auctionDelayMs / 10); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); await runAuction(); if (apiSucceeds) { @@ -264,18 +280,18 @@ describe(`UID2 module`, function () { } else { expectModuleStorageEmptyOrMissing(); } - }, 'the refreshed token should be stored in the module storage', 'the module storage should not be set'); + }, refreshApiUrl, 'the refreshed token should be stored in the module storage', 'the module storage should not be set'); }); describe(`when the response doesn't arrive before the auction timer`, function() { testApiSuccessAndFailure(async function() { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); const bid = await runAuction(); expectNoIdentity(bid); - }, 'it should run the auction'); + }, refreshApiUrl, 'it should run the auction'); testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2); + const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2, server); const bid = await runAuction(); expectNoIdentity(bid); @@ -283,7 +299,7 @@ describe(`UID2 module`, function () { await promise; if (apiSucceeds) expectGlobalToHaveToken(refreshedToken); else expectGlobalToHaveNoUid2(); - }, 'it should update the userId after the auction', 'there should be no global identity'); + }, refreshApiUrl, 'it should update the userId after the auction', 'there should be no global identity'); }) describe('and there is a refreshed token in the module cookie', function() { it('the refreshed value from the cookie is used', async function() { @@ -319,13 +335,13 @@ describe(`UID2 module`, function () { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true), {auctionDelay: 0, syncDelay: 1}); }); testApiSuccessAndFailure(async function() { - apiHelpers.respondAfterDelay(10); + apiHelpers.respondAfterDelay(10, server); const bid = await runAuction(); expectToken(bid, initialToken); - }, 'it should not be refreshed before the auction runs'); + }, refreshApiUrl, 'it should not be refreshed before the auction runs'); testApiSuccessAndFailure(async function(success) { - const promise = apiHelpers.respondAfterDelay(1); + const promise = apiHelpers.respondAfterDelay(1, server); await runAuction(); await promise; if (success) { @@ -333,7 +349,7 @@ describe(`UID2 module`, function () { } else { expectModuleStorageToContain(initialToken, initialToken); } - }, 'the refreshed token should be stored in the module cookie after the auction runs', 'the module cookie should only have the original token'); + }, refreshApiUrl, 'the refreshed token should be stored in the module cookie after the auction runs', 'the module cookie should only have the original token'); it('it should use the current token in the auction', async function() { const bid = await runAuction(); @@ -342,4 +358,277 @@ describe(`UID2 module`, function () { }); }); }); + + if (FEATURES.UID2_CSTG) { + describe('When CSTG is enabled provided', function () { + const scenarios = [ + { + name: 'email provided in config', + identity: { email: 'test@example.com' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, ...this.identity }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test . test@gmail.com' }, extraConfig)) + }, + { + name: 'phone provided in config', + identity: { phone: '+12345678910' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, ...this.identity }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, phone: 'test123' }, extraConfig)) + }, + { + name: 'email hash provided in config', + identity: { email_hash: 'lz3+Rj7IV4X1+Vr1ujkG7tstkxwk5pgkqJ6mXbpOgTs=' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, emailHash: this.identity.email_hash }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, emailHash: 'test@example.com' }, extraConfig)) + }, + { + name: 'phone hash provided in config', + identity: { phone_hash: 'kVJ+4ilhrqm3HZDDnCQy4niZknvCoM4MkoVzZrQSdJw=' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, phoneHash: this.identity.phone_hash }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, phoneHash: '614332222111' }, extraConfig)) + }, + ] + scenarios.forEach(function(scenario) { + describe(`And ${scenario.name}`, function() { + describe(`When invalid identity is provided`, function() { + it('the auction should have no uid2', async function () { + scenario.setInvalidConfig() + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + }); + + describe('When valid identity is provided, and the auction is set to run immediately', function() { + it('it should ignores token provided in config, and the auction should have no uid2', async function() { + scenario.setConfig({ uid2Token: apiHelpers.makeTokenResponse(initialToken), auctionDelay: 0, syncDelay: 1 }); + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + + it('it should ignores token provided in server-set cookie', async function() { + cookieHelpers.setPublisherCookie(publisherCookieName, initialToken); + scenario.setConfig({ ...newServerCookieConfigParams, auctionDelay: 0, syncDelay: 1 }) + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + + describe('When the token generated in time', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + scenario.setConfig(); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, clientSideGeneratedToken); + else expectNoIdentity(bid); + }, cstgApiUrl, 'it should be used in the auction', 'the auction should have no uid2', false, clientSideGeneratedToken); + + testApiSuccessAndFailure(async function(apiSucceeds) { + scenario.setConfig(); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + await runAuction(); + if (apiSucceeds) { + expectModuleStorageToContain(undefined, clientSideGeneratedToken, scenario.identity); + } else { + expectModuleStorageEmptyOrMissing(); + } + }, cstgApiUrl, 'the generated token should be stored in the module storage', 'the module storage should not be set', false, clientSideGeneratedToken); + }); + }); + }); + }); + describe(`when the response doesn't arrive before the auction timer`, function() { + testApiSuccessAndFailure(async function() { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + }, cstgApiUrl, 'it should run the auction', undefined, false, clientSideGeneratedToken); + + testApiSuccessAndFailure(async function(apiSucceeds) { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2, server); + + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + await promise; + if (apiSucceeds) expectGlobalToHaveToken(clientSideGeneratedToken); + else expectGlobalToHaveNoUid2(); + }, cstgApiUrl, 'it should update the userId after the auction', 'there should be no global identity', false, clientSideGeneratedToken); + }) + + describe('when there is a token in the module cookie', function() { + describe('when originalIdentity matches', function() { + describe('When the storedToken is valid', function() { + it('it should use the stored token in the auction', async function() { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com', auctionDelay: 0, syncDelay: 1 })); + const bid = await runAuction(); + expectToken(bid, refreshedToken); + }); + }) + + describe('When the storedToken is expired and can be refreshed ', function() { + it('it should calls refresh API', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, refreshedToken); + else expectNoIdentity(bid); + }, refreshApiUrl, 'it should use refreshed token in the auction', 'the auction should have no uid2'); + }); + }) + + describe('When the storedToken is expired for refresh', function() { + it('it should calls CSTG API and not use the stored token', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken, true, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, clientSideGeneratedToken); + else expectNoIdentity(bid); + }, cstgApiUrl, 'it should use generated token in the auction', 'the auction should have no uid2', false, clientSideGeneratedToken); + }); + }) + }) + + it('when originalIdentity not match, the auction should has no uid2', async function() { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + }); + }) + }); + describe('When invalid CSTG configuration is provided', function () { + const invalidConfigs = [ + { + name: 'CSTG option is not a object', + cstgOptions: '' + }, + { + name: 'CSTG option is null', + cstgOptions: '' + }, + { + name: 'serverPublicKey is not a string', + cstgOptions: { subscriptionId: cstgConfigParams.subscriptionId, serverPublicKey: {} } + }, + { + name: 'serverPublicKey not match regular expression', + cstgOptions: { subscriptionId: cstgConfigParams.subscriptionId, serverPublicKey: 'serverPublicKey' } + }, + { + name: 'subscriptionId is not a string', + cstgOptions: { subscriptionId: {}, serverPublicKey: cstgConfigParams.serverPublicKey } + }, + { + name: 'subscriptionId is empty', + cstgOptions: { subscriptionId: '', serverPublicKey: cstgConfigParams.serverPublicKey } + }, + ] + invalidConfigs.forEach(function(scenario) { + describe(`When ${scenario.name}`, function() { + it('should not generate token using identity', async () => { + config.setConfig(makePrebidConfig({ ...scenario.cstgOptions, email: 'test@email.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }); + }); + }); + }); + describe('When email is provided in different format', function () { + const testCases = [ + { originalEmail: 'TEst.TEST@Test.com ', normalizedEmail: 'test.test@test.com' }, + { originalEmail: 'test+test@test.com', normalizedEmail: 'test+test@test.com' }, + { originalEmail: ' testtest@test.com ', normalizedEmail: 'testtest@test.com' }, + { originalEmail: 'TEst.TEst+123@GMail.Com', normalizedEmail: 'testtest@gmail.com' } + ]; + testCases.forEach((testCase) => { + describe('it should normalize the email and generate token on normalized email', async () => { + testApiSuccessAndFailure(async function(apiSucceeds) { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: testCase.originalEmail })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + await runAuction(); + if (apiSucceeds) { + expectModuleStorageToContain(undefined, clientSideGeneratedToken, { email: testCase.normalizedEmail }); + } else { + expectModuleStorageEmptyOrMissing(); + } + }, cstgApiUrl, 'the generated token should be stored in the module storage', 'the module storage should not be set', false, clientSideGeneratedToken); + }); + }); + }); + } + + describe('When neither token nor CSTG config provided', function () { + describe('when there is a non-cstg-derived token in the module cookie', function () { + it('the auction use stored token if it is valid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken); + const moduleCookie = {originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectToken(bid, initialToken); + }) + + it('the auction should has no uid2 if stored token is invalid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken, true, true, true); + const moduleCookie = {originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) + + describe('when there is a cstg-derived token in the module cookie', function () { + it('the auction use stored token if it is valid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectToken(bid, initialToken); + }) + + it('the auction should has no uid2 if stored token is invalid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken, true, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) + + it('the auction should has no uid2', async function () { + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) }); diff --git a/test/spec/modules/underdogmediaBidAdapter_spec.js b/test/spec/modules/underdogmediaBidAdapter_spec.js index 2d7c1f11178..c0e2e8dddce 100644 --- a/test/spec/modules/underdogmediaBidAdapter_spec.js +++ b/test/spec/modules/underdogmediaBidAdapter_spec.js @@ -5,6 +5,7 @@ import { spec, resetUserSync } from 'modules/underdogmediaBidAdapter.js'; +import { config } from '../../../src/config'; describe('UnderdogMedia adapter', function () { let bidRequests; @@ -763,6 +764,20 @@ describe('UnderdogMedia adapter', function () { expect(request.data.ref).to.equal(undefined); }); + it('should have pbTimeout to be 3001 if bidder timeout does not exists', function () { + config.setConfig({ bidderTimeout: '' }) + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.pbTimeout).to.equal(3001) + }) + + it('should have pbTimeout to be a numerical value if bidder timeout is in a string', function () { + config.setConfig({ bidderTimeout: '1000' }) + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.pbTimeout).to.equal(1000) + }) + it('should have pubcid if it exists', function () { let bidRequests = [{ adUnitCode: 'div-gpt-ad-1460505748561-0', diff --git a/test/spec/modules/undertoneBidAdapter_spec.js b/test/spec/modules/undertoneBidAdapter_spec.js index 7f9c2e7b3d5..5cf53c661a9 100644 --- a/test/spec/modules/undertoneBidAdapter_spec.js +++ b/test/spec/modules/undertoneBidAdapter_spec.js @@ -39,12 +39,19 @@ const videoBidReq = [{ maxDuration: 30 } }, - mediaTypes: {video: { - context: 'outstream', - playerSize: [640, 480], - placement: 1, - plcmt: 1 - }}, + ortb2Imp: { + ext: { + gpid: '/1111/gpid#728x90', + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 1, + plcmt: 1 + } + }, sizes: [[300, 250], [300, 600]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746' @@ -56,10 +63,19 @@ const videoBidReq = [{ placementId: '10433395', publisherId: 12345 }, - mediaTypes: {video: { - context: 'outstream', - playerSize: [640, 480] - }}, + ortb2Imp: { + ext: { + data: { + pbadslot: '/1111/pbadslot#728x90' + } + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, sizes: [[300, 250], [300, 600]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746' @@ -463,12 +479,14 @@ describe('Undertone Adapter', () => { expect(bidVideo.video.skippable).to.equal(true); expect(bidVideo.video.placement).to.equal(1); expect(bidVideo.video.plcmt).to.equal(1); + expect(bidVideo.gpid).to.equal('/1111/gpid#728x90'); expect(bidVideo2.video.skippable).to.equal(null); expect(bidVideo2.video.maxDuration).to.equal(null); expect(bidVideo2.video.playbackMethod).to.equal(null); expect(bidVideo2.video.placement).to.equal(null); expect(bidVideo2.video.plcmt).to.equal(null); + expect(bidVideo2.gpid).to.equal('/1111/pbadslot#728x90'); }); it('should send all userIds data to server', function () { const request = spec.buildRequests(bidReqUserIds, bidderReq); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index e2d4062399b..17a865796a2 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -2,7 +2,7 @@ import { attachIdSystem, auctionDelay, coreStorage, dep, - findRootDomain, + findRootDomain, getConsentHash, init, PBJS_USER_ID_OPTOUT_NAME, requestBidsHook, @@ -12,7 +12,7 @@ import { setSubmoduleRegistry, syncDelay, } from 'modules/userId/index.js'; -import {createEidsArray} from 'modules/userId/eids.js'; +import {createEidsArray, EID_CONFIG} from 'modules/userId/eids.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import {getPrebidInternal} from 'src/utils.js'; @@ -21,7 +21,6 @@ import CONSTANTS from 'src/constants.json'; import {getGlobal} from 'src/prebidGlobal.js'; import {resetConsentData, } from 'modules/consentManagement.js'; import {server} from 'test/mocks/xhr.js'; -import {find} from 'src/polyfill.js'; import {unifiedIdSubmodule} from 'modules/unifiedIdSystem.js'; import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; import {id5IdSubmodule} from 'modules/id5IdSystem.js'; @@ -56,7 +55,7 @@ import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; import {getPPID} from '../../../src/adserver.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; -import {GDPR_GVLIDS} from '../../../src/consentHandler.js'; +import {allConsent, GDPR_GVLIDS, gdprDataHandler} from '../../../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../../../src/activities/modules.js'; import {ACTIVITY_ENRICH_EIDS} from '../../../src/activities/activities.js'; import {ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE} from '../../../src/activities/params.js'; @@ -86,6 +85,20 @@ describe('User ID', function () { } } + function createMockIdSubmodule(name, value, aliasName, eids) { + return { + name, + getId() { + return value; + }, + decode(v) { + return v; + }, + aliasName, + eids + } + } + function getAdUnitMock(code = 'adUnit-code') { return { code, @@ -109,12 +122,6 @@ describe('User ID', function () { return cfg; } - function findEid(eids, source) { - return find(eids, (eid) => { - if (eid.source === source) { return true; } - }); - } - let sandbox, consentData, startDelay, callbackDelay; function clearStack() { @@ -372,11 +379,78 @@ describe('User ID', function () { expect(bid).to.not.have.deep.nested.property('userIdAsEids'); }); }); - // setCookie is called once in order to store consentData - expect(coreStorage.setCookie.callCount).to.equal(1); }); }); + describe('createEidsArray', () => { + beforeEach(() => { + init(config); + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1', null, null, + {'mockId1': {source: 'mock1source', atype: 1}}), + createMockIdSubmodule('mockId2v1', null, null, + {'mockId2v1': {source: 'mock2source', atype: 2, getEidExt: () => ({v: 1})}}), + createMockIdSubmodule('mockId2v2', null, null, + {'mockId2v2': {source: 'mock2source', atype: 2, getEidExt: () => ({v: 2})}}), + ]); + }); + + it('should group UIDs by source and ext', () => { + const eids = createEidsArray({ + mockId1: ['mock-1-1', 'mock-1-2'], + mockId2v1: ['mock-2-1', 'mock-2-2'], + mockId2v2: ['mock-2-1', 'mock-2-2'] + }); + expect(eids).to.eql([ + { + source: 'mock1source', + uids: [ + { + id: 'mock-1-1', + atype: 1, + }, + { + id: 'mock-1-2', + atype: 1, + } + ] + }, + { + source: 'mock2source', + ext: { + v: 1 + }, + uids: [ + { + id: 'mock-2-1', + atype: 2, + }, + { + id: 'mock-2-2', + atype: 2, + } + ] + }, + { + source: 'mock2source', + ext: { + v: 2 + }, + uids: [ + { + id: 'mock-2-1', + atype: 2, + }, + { + id: 'mock-2-2', + atype: 2, + } + ] + } + ]) + }) + }) + it('pbjs.getUserIds', function (done) { init(config); setSubmoduleRegistry([sharedIdSystemSubmodule]); @@ -397,6 +471,139 @@ describe('User ID', function () { }) }); + it('pbjs.getUserIds(Async) should prioritize user ids according to config available to core', () => { + init(config); + + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {mockId1: 'mockId1_value'}}), + createMockIdSubmodule('mockId2Module', {id: {mockId2: 'mockId2_value', mockId3: 'mockId3_value_from_mockId2Module'}}), + createMockIdSubmodule('mockId3Module', {id: {mockId1: 'mockId1_value_from_mockId3Module', mockId2: 'mockId2_value_from_mockId3Module', mockId3: 'mockId3_value', mockId4: 'mockId4_value_from_mockId3Module'}}), + createMockIdSubmodule('mockId4Module', {id: {mockId4: 'mockId4_value'}}) + ]); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId3Module', 'mockId1Module'], + mockId4: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uids) => { + expect(uids['mockId1']).to.deep.equal('mockId1_value_from_mockId3Module'); + expect(uids['mockId2']).to.deep.equal('mockId2_value'); + expect(uids['mockId3']).to.deep.equal('mockId3_value_from_mockId2Module'); + expect(uids['mockId4']).to.deep.equal('mockId4_value'); + }); + }); + + it('pbjs.getUserIds(Async) should prioritize user ids according to config available to core when config uses aliases', () => { + init(config); + + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {mockId1: 'mockId1_value'}}), + createMockIdSubmodule('mockId2Module', {id: {mockId2: 'mockId2_value', mockId3: 'mockId3_value_from_mockId2Module'}}, 'mockId2Module_alias'), + createMockIdSubmodule('mockId3Module', {id: {mockId1: 'mockId1_value_from_mockId3Module', mockId2: 'mockId2_value_from_mockId3Module', mockId3: 'mockId3_value', mockId4: 'mockId4_value_from_mockId3Module'}}, 'mockId3Module_alias'), + createMockIdSubmodule('mockId4Module', {id: {mockId4: 'mockId4_value'}}) + ]); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId3Module_alias', 'mockId1Module'], + mockId4: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uids) => { + expect(uids['mockId1']).to.deep.equal('mockId1_value_from_mockId3Module'); + expect(uids['mockId2']).to.deep.equal('mockId2_value'); + expect(uids['mockId3']).to.deep.equal('mockId3_value_from_mockId2Module'); + expect(uids['mockId4']).to.deep.equal('mockId4_value'); + }); + }); + + it('pbjs.getUserIds(Async) should prioritize user ids according to config available to core when called multiple times', () => { + init(config); + + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {mockId1: 'mockId1_value', mockId2: 'mockId2_value_from_mockId1Module'}}), + createMockIdSubmodule('mockId2Module', {id: {mockId1: 'mockId1_value_from_mockId2Module', mockId2: 'mockId2_value'}}), + ]); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId2Module', 'mockId1Module'], + mockId2: ['mockId1Module', 'mockId2Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uidsFirstRequest) => { + getGlobal().getUserIdsAsync().then((uidsSecondRequest) => { + expect(uidsFirstRequest['mockId1']).to.deep.equal('mockId1_value_from_mockId2Module'); + expect(uidsFirstRequest['mockId2']).to.deep.equal('mockId2_value_from_mockId1Module'); + expect(uidsFirstRequest).to.deep.equal(uidsSecondRequest); + }) + }); + }); + + it('pbjs.getUserIds(Async) with priority config but no collision', () => { + init(config); + + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {mockId1: 'mockId1_value'}}), + createMockIdSubmodule('mockId2Module', {id: {mockId2: 'mockId2_value'}}), + createMockIdSubmodule('mockId3Module', {id: undefined}), + createMockIdSubmodule('mockId4Module', {id: {mockId4: 'mockId4_value'}}) + ]); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId3Module', 'mockId1Module'], + mockId4: ['mockId2Module', 'mockId4Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uids) => { + expect(uids['mockId1']).to.deep.equal('mockId1_value'); + expect(uids['mockId2']).to.deep.equal('mockId2_value'); + expect(uids['mockId3']).to.be.undefined; + expect(uids['mockId4']).to.deep.equal('mockId4_value'); + }); + }); + it('pbjs.getUserIdsAsEids', function (done) { init(config); setSubmoduleRegistry([sharedIdSystemSubmodule]); @@ -416,6 +623,88 @@ describe('User ID', function () { }); }); + it('pbjs.getUserIdsAsEids should prioritize user ids according to config available to core', () => { + init(config); + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value'}}}), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value', lipb: {lipbid: 'lipbid_value_from_mockId2Module'}}}), + createMockIdSubmodule('mockId3Module', {id: {uid2: {id: 'uid2_value_from_mockId3Module'}, pubcid: 'pubcid_value_from_mockId3Module', lipb: {lipbid: 'lipbid_value'}, merkleId: {id: 'merkleId_value_from_mockId3Module'}}}), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value'}}}) + ]); + config.setConfig({ + userSync: { + idPriority: { + uid2: ['mockId3Module', 'mockId1Module'], + merkleId: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + const ids = { + 'uid2': { id: 'uid2_value_from_mockId3Module' }, + 'pubcid': 'pubcid_value', + 'lipb': { lipbid: 'lipbid_value_from_mockId2Module' }, + 'merkleId': { id: 'merkleId_value' } + }; + + return getGlobal().getUserIdsAsync().then(() => { + const eids = getGlobal().getUserIdsAsEids(); + const expected = createEidsArray(ids); + expect(eids).to.deep.equal(expected); + }); + }); + + describe('EID updateConfig', () => { + function mockSubmod(name, eids) { + return createMockIdSubmodule(name, null, null, eids); + } + + it('does not choke if a submod does not provide an eids map', () => { + setSubmoduleRegistry([ + mockSubmod('mock1'), + mockSubmod('mock2') + ]); + expect(EID_CONFIG.size).to.equal(0); + }); + + it('should merge together submodules\' eid configs', () => { + setSubmoduleRegistry([ + mockSubmod('mock1', {mock1: {m: 1}}), + mockSubmod('mock2', {mock2: {m: 2}}) + ]); + expect(EID_CONFIG.get('mock1')).to.eql({m: 1}); + expect(EID_CONFIG.get('mock2')).to.eql({m: 2}); + }); + + it('should respect idPriority', () => { + config.setConfig({ + userSync: { + idPriority: { + m1: ['mod2', 'mod1'], + m2: ['mod1', 'mod2'] + }, + userIds: [ + { name: 'mod1' }, + { name: 'mod2' }, + ] + } + }); + setSubmoduleRegistry([ + mockSubmod('mod1', {m1: {i: 1}, m2: {i: 2}}), + mockSubmod('mod2', {m1: {i: 3}, m2: {i: 4}}) + ]); + expect(EID_CONFIG.get('m1')).to.eql({i: 3}); + expect(EID_CONFIG.get('m2')).to.eql({i: 2}); + }); + }) + it('should set googletag ppid correctly', function () { let adUnits = [getAdUnitMock()]; init(config); @@ -441,6 +730,59 @@ describe('User ID', function () { }); }); + it('should set googletag ppid correctly when prioritized according to config available to core', () => { + let adUnits = [getAdUnitMock()]; + init(config); + setSubmoduleRegistry([ + // some of the ids are padded to have length >= 32 characters + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value_7ac66c0f148de9519b8bd264312c4d64'}}}), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value_7ac66c0f148de9519b8bd264312c4d64', lipb: {lipbid: 'lipbid_value_from_mockId2Module_7ac66c0f148de9519b8bd264312c4d64'}}}), + createMockIdSubmodule('mockId3Module', { + id: { + uid2: { + id: 'uid2_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64' + }, + pubcid: 'pubcid_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64', + lipb: { + lipbid: 'lipbid_value_7ac66c0f148de9519b8bd264312c4d64' + }, + merkleId: { + id: 'merkleId_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64' + } + } + }, null, { + uid2: { + source: 'uidapi.com', + getValue(data) { return data.id } + } + }), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value_7ac66c0f148de9519b8bd264312c4d64'}}}) + ]); + + // before ppid should not be set + expect(window.googletag._ppid).to.equal(undefined); + + config.setConfig({ + userSync: { + ppid: 'uidapi.com', + idPriority: { + uid2: ['mockId3Module', 'mockId1Module'], + merkleId: ['mockId4Module', 'mockId3Module'] + }, + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return expectImmediateBidHook(() => {}, {adUnits}).then(() => { + expect(window.googletag._ppid).to.equal('uid2valuefrommockId3Module7ac66c0f148de9519b8bd264312c4d64'); + }); + }); + describe('submodule callback', () => { const TEST_KEY = 'testKey'; @@ -504,6 +846,11 @@ describe('User ID', function () { }, decode(d) { return d + }, + eids: { + pubcid: { + source: 'pubcid.org', + } } }]); config.setConfig({ @@ -589,6 +936,58 @@ describe('User ID', function () { }) }); + it('should make PPID available to core and respect priority', () => { + init(config); + setSubmoduleRegistry([ + // some of the ids are padded to have length >= 32 characters + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value_7ac66c0f148de9519b8bd264312c4d64'}}}), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value_7ac66c0f148de9519b8bd264312c4d64', lipb: {lipbid: 'lipbid_value_from_mockId2Module_7ac66c0f148de9519b8bd264312c4d64'}}}), + createMockIdSubmodule('mockId3Module', { + id: { + uid2: { + id: 'uid2_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64' + }, + pubcid: 'pubcid_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64', + lipb: { + lipbid: 'lipbid_value_7ac66c0f148de9519b8bd264312c4d64' + }, + merkleId: { + id: 'merkleId_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64' + } + } + }, null, { + uid2: { + source: 'uidapi.com', + getValue(data) { return data.id } + } + }), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value_7ac66c0f148de9519b8bd264312c4d64'}}}) + ]); + + // before ppid should not be set + expect(window.googletag._ppid).to.equal(undefined); + + config.setConfig({ + userSync: { + ppid: 'uidapi.com', + idPriority: { + uid2: ['mockId3Module', 'mockId1Module'], + merkleId: ['mockId4Module', 'mockId3Module'] + }, + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().refreshUserIds().then(() => { + expect(getPPID()).to.eql('uid2valuefrommockId3Module7ac66c0f148de9519b8bd264312c4d64'); + }) + }); + describe('refreshing before init is complete', () => { const MOCK_ID = {'MOCKID': '1111'}; let mockIdCallback; @@ -596,6 +995,7 @@ describe('User ID', function () { beforeEach(() => { mockIdCallback = sinon.stub(); + coreStorage.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); let mockIdSystem = { name: 'mockId', decode: function(value) { @@ -763,9 +1163,66 @@ describe('User ID', function () { }); }); + it('pbjs.refreshUserIds updates priority config', () => { + init(config); + + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {mockId1: 'mockId1_value'}}), + createMockIdSubmodule('mockId2Module', {id: {mockId2: 'mockId2_value', mockId3: 'mockId3_value_from_mockId2Module'}}), + createMockIdSubmodule('mockId3Module', {id: {mockId1: 'mockId1_value_from_mockId3Module', mockId2: 'mockId2_value_from_mockId3Module', mockId3: 'mockId3_value', mockId4: 'mockId4_value_from_mockId3Module'}}), + createMockIdSubmodule('mockId4Module', {id: {mockId4: 'mockId4_value'}}) + ]); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId3Module', 'mockId1Module'], + mockId4: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uids) => { + expect(uids['mockId1']).to.deep.equal('mockId1_value_from_mockId3Module'); + expect(uids['mockId2']).to.deep.equal('mockId2_value'); + expect(uids['mockId3']).to.deep.equal('mockId3_value_from_mockId2Module'); + expect(uids['mockId4']).to.deep.equal('mockId4_value'); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId1Module', 'mockId3Module'], + mockId4: ['mockId3Module', 'mockId4Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uids) => { + expect(uids['mockId1']).to.deep.equal('mockId1_value'); + expect(uids['mockId2']).to.deep.equal('mockId2_value'); + expect(uids['mockId3']).to.deep.equal('mockId3_value_from_mockId2Module'); + expect(uids['mockId4']).to.deep.equal('mockId4_value_from_mockId3Module'); + }); + }); + }); + it('pbjs.refreshUserIds refreshes single', function() { coreStorage.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('REFRESH', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('refreshedid', '', EXPIRED_COOKIE_DATE); let sandbox = sinon.createSandbox(); let mockIdCallback = sandbox.stub().returns({id: {'MOCKID': '1111'}}); @@ -2615,7 +3072,7 @@ describe('User ID', function () { expect(server.requests).to.be.empty; return endAuction(); }).then(() => { - expect(server.requests[0].url).to.equal('/any/unifiedid/url'); + expect(server.requests[0].url).to.match(/\/any\/unifiedid\/url/); }); }); @@ -2729,17 +3186,12 @@ describe('User ID', function () { } }; - consentData = { - gdprApplies: true, - consentString: 'mockString', - apiVersion: 1, - hasValidated: true // mock presence of GPDR enforcement module - } // clear cookies expStr = (new Date(Date.now() + 25000).toUTCString()); coreStorage.setCookie(mockIdCookieName, '', EXPIRED_COOKIE_DATE); coreStorage.setCookie(`${mockIdCookieName}_last`, '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie(CONSENT_LOCAL_STORAGE_NAME, '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie(`${mockIdCookieName}_cst`, '', EXPIRED_COOKIE_DATE); + allConsent.reset(); // init adUnits = [getAdUnitMock()]; @@ -2753,10 +3205,20 @@ describe('User ID', function () { config.resetConfig(); }); - it('calls getId if no stored consent data and refresh is not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); - coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); + function setStorage({ + val = JSON.stringify({id: '1234'}), + lastDelta = 60 * 1000, + cst = null + } = {}) { + coreStorage.setCookie(mockIdCookieName, val, expStr); + coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - lastDelta).toUTCString()), expStr); + if (cst != null) { + coreStorage.setCookie(`${mockIdCookieName}_cst`, cst, expStr); + } + } + it('calls getId if no stored consent data and refresh is not needed', function () { + setStorage({lastDelta: 1000}); config.setConfig(userIdConfig); let innerAdUnits; @@ -2770,9 +3232,7 @@ describe('User ID', function () { }); it('calls getId if no stored consent data but refresh is needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); - coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 60 * 1000).toUTCString()), expStr); - + setStorage(); config.setConfig(userIdConfig); let innerAdUnits; @@ -2786,11 +3246,7 @@ describe('User ID', function () { }); it('calls getId if empty stored consent and refresh not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); - coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); - - setStoredConsentData(); - + setStorage({cst: ''}); config.setConfig(userIdConfig); let innerAdUnits; @@ -2804,10 +3260,10 @@ describe('User ID', function () { }); it('calls getId if stored consent does not match current consent and refresh not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); - coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); - - setStoredConsentData({...consentData, consentString: 'different'}); + setStorage({cst: getConsentHash()}); + gdprDataHandler.setConsentData({ + consentString: 'different' + }); config.setConfig(userIdConfig); @@ -2822,10 +3278,7 @@ describe('User ID', function () { }); it('does not call getId if stored consent matches current consent and refresh not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); - coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); - - setStoredConsentData({...consentData}); + setStorage({lastDelta: 1000, cst: getConsentHash()}); config.setConfig(userIdConfig); @@ -2950,6 +3403,73 @@ describe('User ID', function () { }).catch(done); }); + it('pbjs.getEncryptedEidsForSource should return prioritized id as non-encrypted string', (done) => { + init(config); + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value'}}}), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value', lipb: {lipbid: 'lipbid_value_from_mockId2Module'}}}), + createMockIdSubmodule('mockId3Module', {id: {uid2: {id: 'uid2_value_from_mockId3Module'}, pubcid: 'pubcid_value_from_mockId3Module', lipb: {lipbid: 'lipbid_value'}, merkleId: {id: 'merkleId_value_from_mockId3Module'}}}, null, { + uid2: { + source: 'uidapi.com', + getValue(data) { + return data.id + } + }, + pubcid: { + source: 'pubcid.org', + }, + lipb: { + source: 'liveintent.com', + getValue(data) { + return data.lipbid + } + } + }), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value'}}}, null, { + merkleId: { + source: 'merkleinc.com', + getValue(data) { + return data.id + } + } + }) + ]); + config.setConfig({ + userSync: { + idPriority: { + uid2: ['mockId3Module', 'mockId1Module'], + merkleId: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + const expctedIds = [ + 'pubcid_value', + 'uid2_value_from_mockId3Module', + 'merkleId_value', + 'lipbid_value_from_mockId2Module' + ]; + + const encrypt = false; + + Promise.all([ + getGlobal().getEncryptedEidsForSource('pubcid.org', encrypt), + getGlobal().getEncryptedEidsForSource('uidapi.com', encrypt), + getGlobal().getEncryptedEidsForSource('merkleinc.com', encrypt), + getGlobal().getEncryptedEidsForSource('liveintent.com', encrypt) + ]).then((result) => { + expect(result).to.deep.equal(expctedIds); + done(); + }) + }); + describe('pbjs.getEncryptedEidsForSource', () => { beforeEach(() => { init(config); @@ -3013,6 +3533,45 @@ describe('User ID', function () { done(); }); }); + + it('pbjs.getUserIdsAsEidBySource with priority config available to core', () => { + init(config); + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value'}}}), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value', lipb: {lipbid: 'lipbid_value_from_mockId2Module'}}}), + createMockIdSubmodule('mockId3Module', {id: {uid2: {id: 'uid2_value_from_mockId3Module'}, pubcid: 'pubcid_value_from_mockId3Module', lipb: {lipbid: 'lipbid_value'}, merkleId: {id: 'merkleId_value_from_mockId3Module'}}}), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value'}}}) + ]); + config.setConfig({ + userSync: { + idPriority: { + uid2: ['mockId3Module', 'mockId1Module'], + merkleId: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + const ids = { + 'uidapi.com': {'uid2': {id: 'uid2_value_from_mockId3Module'}}, + 'pubcid.org': {'pubcid': 'pubcid_value'}, + 'liveintent.com': {'lipb': {lipbid: 'lipbid_value_from_mockId2Module'}}, + 'merkleinc.com': {'merkleId': {id: 'merkleId_value'}} + }; + + return getGlobal().getUserIdsAsync().then(() => { + expect(getGlobal().getUserIdsAsEidBySource('pubcid.org')).to.deep.equal(createEidsArray(ids['pubcid.org'])[0]); + expect(getGlobal().getUserIdsAsEidBySource('uidapi.com')).to.deep.equal(createEidsArray(ids['uidapi.com'])[0]); + expect(getGlobal().getUserIdsAsEidBySource('merkleinc.com')).to.deep.equal(createEidsArray(ids['merkleinc.com'])[0]); + expect(getGlobal().getUserIdsAsEidBySource('liveintent.com')).to.deep.equal(createEidsArray(ids['liveintent.com'])[0]); + }); + }); }) }); }); diff --git a/test/spec/modules/viantOrtbBidAdapter_spec.js b/test/spec/modules/viantOrtbBidAdapter_spec.js new file mode 100644 index 00000000000..ef537d50986 --- /dev/null +++ b/test/spec/modules/viantOrtbBidAdapter_spec.js @@ -0,0 +1,432 @@ +import { spec, converter } from 'modules/viantOrtbBidAdapter.js'; +import {assert, expect} from 'chai'; +import { deepClone } from '../../../src/utils'; +import {buildWindowTree} from '../../helpers/refererDetectionHelper'; +import {detectReferer} from '../../../src/refererDetection'; +describe('viantOrtbBidAdapter', function () { + function testBuildRequests(bidRequests, bidderRequestBase) { + let clonedBidderRequest = deepClone(bidderRequestBase); + clonedBidderRequest.bids = bidRequests; + let requests = spec.buildRequests(bidRequests, clonedBidderRequest); + return requests + } + describe('isBidRequestValid', function() { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': 'some-PlacementId_1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [728, 90] + ] + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + } + + describe('core', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when publisherId not passed', function () { + let bid = makeBid(); + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true if placementId is not passed ', function () { + let bid = makeBid(); + delete bid.params.placementId; + bid.ortb2Imp = { + + } + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if mediaTypes.banner is Not passed', function () { + let bid = makeBid(); + delete bid.mediaTypes + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('banner', function () { + it('should return true if banner.pos is passed correctly', function () { + let bid = makeBid(); + bid.mediaTypes.banner.pos = 1; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('video', function () { + describe('and request config uses mediaTypes', () => { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'publisherId': '464', + 'placementId': 'some-PlacementId_2' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 30 + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + } + } + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let videoBidWithMediaTypes = Object.assign({}, makeBid()); + videoBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(videoBidWithMediaTypes)).to.equal(false); + }); + }); + }); + }); + + describe('buildRequests-banner', function () { + const baseBannerBidRequests = [{ + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': '1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + } + }, + 'gdprConsent': { + 'consentString': 'consentString', + 'gdprApplies': true, + }, + 'uspConsent': '1YYY', + 'sizes': [[728, 90]], + 'transactionId': '1111474f-58b1-4368-b812-84f8c937a099', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const testWindow = buildWindowTree(['https://www.example.com/test', 'https://www.example.com/other/page', 'https://www.example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'); + const baseBidderRequestReferer = detectReferer(testWindow)(); + const baseBidderRequest = { + 'bidderCode': 'viant', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': baseBidderRequestReferer, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('test regs', function () { + const gdprBaseBidderRequest = Object.assign({}, baseBidderRequest, { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, + }, + uspConsent: '1YYN' + }); + const request = testBuildRequests(baseBannerBidRequests, gdprBaseBidderRequest)[0]; + expect(request.data.regs.ext).to.have.property('gdpr', 1); + expect(request.data.regs.ext).to.have.property('us_privacy', '1YYN'); + }); + + it('sends bid request to our endpoint that makes sense', function () { + const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.be.not.empty; + expect(request.data).to.be.not.null; + }); + it('sends bid requests to the correct endpoint', function () { + const url = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0].url; + expect(url).to.equal('https://bidders-us-east-1.adelphic.net/d/rtb/v25/prebid/bidder_test'); + }); + + it('sends site', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0].data; + expect(requestBody.site).to.be.not.null; + }); + + it('includes the ad size in the bid request', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0].data; + expect(requestBody.imp[0].banner.format[0].w).to.equal(728); + expect(requestBody.imp[0].banner.format[0].h).to.equal(90); + }); + + it('sets the banner pos correctly if sent', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + clonedBannerRequests[0].mediaTypes.banner.pos = 1; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest)[0].data; + expect(requestBody.imp[0].banner.pos).to.equal(1); + }); + }); + + if (FEATURES.VIDEO) { + describe('buildRequests-video', function () { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'publisherId': '464', + 'placementId': 'some-PlacementId_2' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 31 + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + } + } + + it('assert video and its fields is present in imp ', function () { + let requests = spec.buildRequests([makeBid()], {referrerInfo: {}}); + let clonedRequests = deepClone(requests) + assert.equal(clonedRequests[0].data.imp[0].video.mimes[0], 'video/mp4') + assert.equal(clonedRequests[0].data.imp[0].video.maxduration, 31) + assert.equal(clonedRequests[0].data.imp[0].video.placement, 1) + assert.equal(clonedRequests[0].method, 'POST') + }); + }); + } + + describe('interpretResponse', function () { + const baseBannerBidRequests = [{ + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': '1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + } + }, + 'sizes': [[728, 90]], + 'transactionId': '1111474f-58b1-4368-b812-84f8c937a099', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const testWindow = buildWindowTree(['https://www.example.com/test', 'https://www.example.com/other/page', 'https://www.example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'); + const baseBidderRequestReferer = detectReferer(testWindow)(); + const baseBidderRequest = { + 'bidderCode': 'viant', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': baseBidderRequestReferer, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('empty bid response test', function () { + const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0]; + let bidResponse = {nbr: 0}; // Unknown error + let bids = spec.interpretResponse({body: bidResponse}, request); + expect(bids.length).to.equal(0); + }); + + it('bid response is a banner', function () { + const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0]; + let bidResponse = { + seatbid: [{ + bid: [{ + impid: '243310435309b5', + price: 2, + w: 728, + h: 90, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup', + }] + }], + cur: 'USD' + }; + let bids = spec.interpretResponse({body: bidResponse}, request); + expect(bids.length).to.equal(1); + let bid = bids[0]; + it('should return the proper mediaType', function () { + it('should return a creativeId', function () { + expect(bid.mediaType).to.equal('banner'); + }); + }); + it('should return a price', function () { + expect(bid.cpm).to.equal(bidResponse.seatbid[0].bid[0].price); + }); + + it('should return a request id', function () { + expect(bid.requestId).to.equal(bidResponse.seatbid[0].bid[0].impid); + }); + + it('should return width and height for the creative', function () { + expect(bid.width).to.equal(bidResponse.seatbid[0].bid[0].w); + expect(bid.height).to.equal(bidResponse.seatbid[0].bid[0].h); + }); + it('should return a creativeId', function () { + expect(bid.creativeId).to.equal(bidResponse.seatbid[0].bid[0].crid); + }); + it('should return an ad', function () { + expect(bid.ad).to.equal(bidResponse.seatbid[0].bid[0].adm); + }); + + it('should return a deal id if it exists', function () { + expect(bid.dealId).to.equal(bidResponse.seatbid[0].bid[0].dealid); + }); + + it('should have a time-to-live of 5 minutes', function () { + expect(bid.ttl).to.equal(300); + }); + + it('should always return net revenue', function () { + expect(bid.netRevenue).to.equal(true); + }); + it('should return a currency', function () { + expect(bid.currency).to.equal(bidResponse.cur); + }); + }); + }); + describe('interpretResponse-Video', function () { + const baseVideoBidRequests = [{ + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': '1' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 31 + } + }, + 'sizes': [[640, 480]], + 'transactionId': '1111474f-58b1-4368-b812-84f8c937a099', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const testWindow = buildWindowTree(['https://www.example.com/test', 'https://www.example.com/other/page', 'https://www.example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'); + const baseBidderRequestReferer = detectReferer(testWindow)(); + const baseBidderRequest = { + 'bidderCode': 'viant', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': baseBidderRequestReferer, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('bid response is a video', function () { + const request = testBuildRequests(baseVideoBidRequests, baseBidderRequest)[0]; + const VIDEO_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidid': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'seatbid': [ + { + 'bid': [ + { + 'id': '1', + 'impid': '243310435309b5', + 'price': 1.09, + 'adid': '144762342', + 'nurl': 'http://0.0.0.0:8181/nurl', + 'adm': '', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'h': 480, + 'w': 640 + } + ] + } + ], + 'cur': 'USD' + }; + let bids = spec.interpretResponse({body: VIDEO_BID_RESPONSE}, request); + expect(bids.length).to.equal(1); + let bid = bids[0]; + it('should return the proper mediaType', function () { + expect(bid.mediaType).to.equal('video'); + }); + it('should return correct Ad Markup', function () { + expect(bid.vastXml).to.equal(''); + }); + it('should return correct Notification', function () { + expect(bid.vastUrl).to.equal('http://0.0.0.0:8181/nurl'); + }); + it('should return correct Cpm', function () { + expect(bid.cpm).to.equal(1.09); + }); + }); + }); +}); diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 6e92bae8d59..864f2b8551c 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -40,7 +40,6 @@ const BID = { 'placementCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250], [300, 600]], 'bidderRequestId': '1fdb5ff1b6eaa7', - 'auctionId': 'auction_id', 'bidRequestsCount': 4, 'bidderRequestsCount': 3, 'bidderWinsCount': 1, @@ -64,7 +63,6 @@ const VIDEO_BID = { tid: '56e184c6-bde9-497b-b9b9-cf47a61381ee', } }, - 'auctionId': 'auction_id', 'bidRequestsCount': 4, 'bidderRequestsCount': 3, 'bidderWinsCount': 1, @@ -142,6 +140,7 @@ const SERVER_RESPONSE = { cid: 'testcid123', results: [{ 'ad': '', + 'bidId': '2d52001cabd527-response', 'price': 0.8, 'creativeId': '12610997325162499419', 'exp': 30, @@ -287,7 +286,6 @@ describe('VidazooBidAdapter', function () { usPrivacy: 'consent_string', gppString: 'gpp_string', gppSid: [7], - auctionId: 'auction_id', bidRequestsCount: 4, bidderRequestsCount: 3, bidderWinsCount: 1, @@ -361,7 +359,6 @@ describe('VidazooBidAdapter', function () { usPrivacy: 'consent_string', gppString: 'gpp_string', gppSid: [7], - auctionId: 'auction_id', bidRequestsCount: 4, bidderRequestsCount: 3, bidderWinsCount: 1, @@ -413,8 +410,122 @@ describe('VidazooBidAdapter', function () { }); }); + it('should build single banner request for multiple bids', function () { + config.setConfig({ + bidderTimeout: 3000, + vidazoo: { + singleRequest: true, + chunkSize: 2 + } + }); + + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + + const BID2 = utils.deepClone(BID); + BID2.bidId = '2d52001cabd528'; + BID2.adUnitCode = 'div-gpt-ad-12345-1'; + BID2.sizes = [[300, 250]]; + + const REQUEST_DATA = { + gdprConsent: 'consent_string', + gdpr: 1, + usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + bidderRequestId: '1fdb5ff1b6eaa7', + sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + cb: 1000, + bidFloor: 0.1, + bidId: '2d52001cabd527', + adUnitCode: 'div-gpt-ad-12345-0', + publisherId: '59ac17c192832d0011283fe3', + dealId: 3, + sessionId: '', + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + bidderVersion: adapter.version, + prebidVersion: version, + schain: BID.schain, + ptrace: '1000', + res: `${window.top.screen.width}x${window.top.screen.height}`, + mediaTypes: [BANNER], + uqs: getTopWindowQueryParams(), + 'ext.param1': 'loremipsum', + 'ext.param2': 'dolorsitamet', + isStorageAllowed: true, + gpid: '1234567890', + cat: ['IAB2'], + pagecat: ['IAB2-2'], + webSessionId: webSessionId + }; + + const REQUEST_DATA2 = utils.deepClone(REQUEST_DATA); + REQUEST_DATA2.bidId = '2d52001cabd528'; + REQUEST_DATA2.adUnitCode = 'div-gpt-ad-12345-1'; + REQUEST_DATA2.sizes = ['300x250']; + REQUEST_DATA2.dealId = 4; + + const requests = adapter.buildRequests([BID, BID2], BIDDER_REQUEST); + expect(requests).to.have.length(1); + + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, + data: {bids: [REQUEST_DATA, REQUEST_DATA2]} + }); + }); + + it('should return seperated requests for video and banner if singleRequest is true', function () { + config.setConfig({ + bidderTimeout: 3000, + vidazoo: { + singleRequest: true, + chunkSize: 2 + } + }); + + const requests = adapter.buildRequests([BID, VIDEO_BID], BIDDER_REQUEST); + expect(requests).to.have.length(2); + }); + + it('should chunk requests if requests exceed chunkSize and singleRequest is true', function () { + config.setConfig({ + bidderTimeout: 3000, + vidazoo: { + singleRequest: true, + chunkSize: 2 + } + }); + + const requests = adapter.buildRequests([BID, BID, BID, BID], BIDDER_REQUEST); + expect(requests).to.have.length(2); + }); + after(function () { $$PREBID_GLOBAL$$.bidderSettings = {}; + config.resetConfig(); sandbox.restore(); }); }); @@ -515,6 +626,21 @@ describe('VidazooBidAdapter', function () { }); }); + it('should populate requestId from response in case of singleRequest true', function () { + config.setConfig({ + vidazoo: { + singleRequest: true, + chunkSize: 2 + } + }); + + const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0].requestId).to.equal('2d52001cabd527-response'); + + config.resetConfig(); + }); + it('should take default TTL', function () { const serverResponse = utils.deepClone(SERVER_RESPONSE); delete serverResponse.body.results[0].exp; diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index 9a486cd6c34..139349ceead 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec } from 'modules/visxBidAdapter.js'; +import { spec, storage } from 'modules/visxBidAdapter.js'; import { config } from 'src/config.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; @@ -82,6 +82,9 @@ describe('VisxAdapter', function () { }); return res; } + + let cookiesAreEnabledStub, localStorageIsEnabledStub; + const bidderRequest = { timeout: 3000, refererInfo: { @@ -180,6 +183,24 @@ describe('VisxAdapter', function () { 'ext': {'bidder': {'uid': 903537}} }]; + before(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); + }); + + after(() => { + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should attach valid params to the tag', function () { const firstBid = bidRequests[0]; const bids = [firstBid]; @@ -422,6 +443,7 @@ describe('VisxAdapter', function () { }); return res; } + let cookiesAreEnabledStub, localStorageIsEnabledStub; const bidderRequest = { timeout: 3000, refererInfo: { @@ -449,6 +471,24 @@ describe('VisxAdapter', function () { } ]; + before(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); + }); + + after(() => { + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should send requst for banner bid', function () { const request = spec.buildRequests([bidRequests[0]], bidderRequest); const payload = parseRequest(request.url); @@ -486,6 +526,7 @@ describe('VisxAdapter', function () { }); return res; } + let cookiesAreEnabledStub, localStorageIsEnabledStub; const bidderRequest = { timeout: 3000, refererInfo: { @@ -529,10 +570,23 @@ describe('VisxAdapter', function () { documentStub.withArgs('visx-adunit-element-2').returns({ id: 'visx-adunit-element-2' }); + + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); }); after(function() { sandbox.restore(); + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; }); it('should find ad slot by ad unit code as element id', function () { @@ -1323,4 +1377,100 @@ describe('VisxAdapter', function () { expect(query).to.deep.equal({}); }); }); + + describe('first party user id', function () { + const USER_ID_KEY = '__vads'; + const USER_ID_DUMMY_VALUE_COOKIE = 'dummy_id_cookie'; + const USER_ID_DUMMY_VALUE_LOCAL_STORAGE = 'dummy_id_local_storage'; + + let getDataFromLocalStorageStub, localStorageIsEnabledStub; + let getCookieStub, cookiesAreEnabledStub; + + const bidRequests = [ + { + 'bidder': 'visx', + 'params': { + 'uid': 903535 + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + const bidderRequest = { + timeout: 3000, + refererInfo: { + page: 'https://example.com' + } + }; + + beforeEach(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: true + } + }; + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + }); + + afterEach(() => { + cookiesAreEnabledStub.restore(); + localStorageIsEnabledStub.restore(); + getCookieStub && getCookieStub.restore(); + getDataFromLocalStorageStub && getDataFromLocalStorageStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + + it('should not pass user id if both cookies and local storage are not available', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(false); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user).to.be.undefined; + }); + + it('should get user id from cookie if available', function () { + cookiesAreEnabledStub.returns(true); + localStorageIsEnabledStub.returns(false); + getCookieStub = sinon.stub(storage, 'getCookie'); + getCookieStub.withArgs(USER_ID_KEY).returns(USER_ID_DUMMY_VALUE_COOKIE); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user.ext.vads).to.equal(USER_ID_DUMMY_VALUE_COOKIE); + }); + + it('should get user id from local storage if available', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(true); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorageStub.withArgs(USER_ID_KEY).returns(USER_ID_DUMMY_VALUE_LOCAL_STORAGE); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user.ext.vads).to.equal(USER_ID_DUMMY_VALUE_LOCAL_STORAGE); + }); + + it('should create user id and store it in cookies (if user id does not exist)', function () { + cookiesAreEnabledStub.returns(true); + localStorageIsEnabledStub.returns(false); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(storage.getCookie(USER_ID_KEY)).to.be.a('string'); + expect(request.data.user.ext.vads).to.be.a('string'); + }); + + it('should create user id and store it in local storage (if user id does not exist)', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(true); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(storage.getDataFromLocalStorage(USER_ID_KEY)).to.be.a('string'); + expect(request.data.user.ext.vads).to.be.a('string'); + }); + }); }); diff --git a/test/spec/modules/voxBidAdapter_spec.js b/test/spec/modules/voxBidAdapter_spec.js index 923b0465e6c..5f4ada06c65 100644 --- a/test/spec/modules/voxBidAdapter_spec.js +++ b/test/spec/modules/voxBidAdapter_spec.js @@ -1,5 +1,6 @@ import { expect } from 'chai' import { spec } from 'modules/voxBidAdapter.js' +import {config} from 'src/config.js' function getSlotConfigs(mediaTypes, params) { return { @@ -175,6 +176,98 @@ describe('VOX Adapter', function() { expect(bid.transactionId).to.equal('31a58515-3634-4e90-9c96-f86196db1459') }) }) + it('should not set userid if not specified', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.userId).to.be.undefined + }) + }) + + it('should set userid if specified', function () { + const requests = validBidRequests.map(bid => ({ + ...bid, + userId: { + tdid: 'TDID_USER_ID', + pubcid: 'PUBID_USER_ID' + } + })) + const request = spec.buildRequests(requests, bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.userId.tdid).to.equal('TDID_USER_ID') + expect(bid.userId.pubcid).to.equal('PUBID_USER_ID') + }) + }) + + it('should not set schain if not specified', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.schain).to.be.undefined + }) + }) + + it('should set schain if not specified', function () { + const requests = validBidRequests.map(bid => ({ + ...bid, + schain: { + validation: 'strict', + config: { + ver: '1.0' + } + } + })) + const request = spec.buildRequests(requests, bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.schain.validation).to.equal('strict') + expect(bid.schain.config.ver).to.equal('1.0') + }) + }) + + describe('price floors', function () { + it('should be empty if floors module not configured', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.floorInfo).to.be.empty + }) + }) + + it('should add correct floor values', function () { + const expectedFloors = [ 2, 2.7, 1.4 ] + const validBidRequests = expectedFloors.map(getBidWithFloor) + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expectedFloors.forEach((floor, index) => { + expect(data.bidRequests[index].floorInfo.floor).to.equal(floor) + expect(data.bidRequests[index].floorInfo.currency).to.equal('USD') + }) + }) + + it('should request floor price in adserver currency', function () { + const configCurrency = 'DKK' + config.setConfig({ currency: { adServerCurrency: configCurrency } }) + const request = spec.buildRequests([ getBidWithFloor() ], bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.floorInfo.currency).to.equal(configCurrency) + }) + }) + + function getBidWithFloor(floor) { + return { + ...validBidRequests[0], + getFloor: ({ currency }) => { + return { + currency: currency, + floor + } + } + } + } + }) describe('GDPR params', function() { describe('when there are not consent management platform', function() { diff --git a/test/spec/modules/vrtcalBidAdapter_spec.js b/test/spec/modules/vrtcalBidAdapter_spec.js index 764db3fa4f0..938934170e9 100644 --- a/test/spec/modules/vrtcalBidAdapter_spec.js +++ b/test/spec/modules/vrtcalBidAdapter_spec.js @@ -29,7 +29,12 @@ describe('vrtcalBidAdapter', function () { 'bidderRequestId': 'br0001', 'auctionId': 'auction0001', 'userIdAsEids': {}, - timeout: 435 + timeout: 435, + + refererInfo: { + page: 'page' + } + } ]; @@ -79,9 +84,12 @@ describe('vrtcalBidAdapter', function () { }); it('pass 3rd party IDs with the request when present', function () { - bidRequests[0].userIdAsEids = createEidsArray({ - tdid: 'TTD_ID_FROM_USER_ID_MODULE' - }); + bidRequests[0].userIdAsEids = [ + { + source: 'adserver.org', + uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}] + } + ]; request = spec.buildRequests(bidRequests); expect(request[0].data).to.include(JSON.stringify({ext: {consent: 'gdpr-consent-string', eids: [{source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}]}})); @@ -126,4 +134,39 @@ describe('vrtcalBidAdapter', function () { ).to.be.true }) }) + + describe('getUserSyncs', function() { + const syncurl_iframe = 'https://usync.vrtcal.com/i?ssp=1804&synctype=iframe'; + const syncurl_redirect = 'https://usync.vrtcal.com/i?ssp=1804&synctype=redirect'; + + it('base iframe sync pper config', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('base redirect sync per config', function() { + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ + type: 'image', url: syncurl_redirect + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with ccpa data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, 'ccpa_consent_string', undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=ccpa_consent_string&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with gdpr data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: 1, consentString: 'gdpr_consent_string'}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=1&gdpr_consent=gdpr_consent_string&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with gpp data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined, {gppString: 'gpp_consent_string', applicableSections: [1, 5]})).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=gpp_consent_string&gpp_sid=1,5&surl=' + }]); + }); + }) }) diff --git a/test/spec/modules/xeBidAdapter_spec.js b/test/spec/modules/xeBidAdapter_spec.js index 38b334c32c5..914b0cacd71 100644 --- a/test/spec/modules/xeBidAdapter_spec.js +++ b/test/spec/modules/xeBidAdapter_spec.js @@ -41,414 +41,412 @@ defaultRequestVideo.mediaTypes = { skipppable: true } }; +describe('xeBidAdapter', () => { + describe('isBidRequestValid', function () { + it('should return false when request params is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); -describe('isBidRequestValid', function () { - it('should return false when request params is missing', function () { - const invalidRequest = deepClone(defaultRequest); - delete invalidRequest.params; - expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); - }); - - it('should return false when required env param is missing', function () { - const invalidRequest = deepClone(defaultRequest); - delete invalidRequest.params.env; - expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); - }); + it('should return false when required env param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.env; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); - it('should return false when required placement param is missing', function () { - const invalidRequest = deepClone(defaultRequest); - delete invalidRequest.params.placement; - expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); - }); + it('should return false when required placement param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.placement; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); - it('should return false when video.playerSize is missing', function () { - const invalidRequest = deepClone(defaultRequestVideo); - delete invalidRequest.mediaTypes.video.playerSize; - expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); - }); + it('should return false when video.playerSize is missing', function () { + const invalidRequest = deepClone(defaultRequestVideo); + delete invalidRequest.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); - it('should return true when required params found', function () { - expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); + }); }); -}); -describe('buildRequests', function () { - beforeEach(function () { - config.resetConfig(); - }); + describe('buildRequests', function () { + beforeEach(function () { + config.resetConfig(); + }); - it('should send request with correct structure', function () { - const request = spec.buildRequests([defaultRequest], {}); - expect(request.method).to.equal('POST'); - expect(request.url).to.equal(ENDPOINT); - expect(request.options).to.have.property('contentType').and.to.equal('application/json'); - expect(request).to.have.property('data'); - }); + it('should send request with correct structure', function () { + const request = spec.buildRequests([defaultRequest], {}); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT); + expect(request.options).to.have.property('contentType').and.to.equal('application/json'); + expect(request).to.have.property('data'); + }); - it('should build basic request structure', function () { - const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; - expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); - expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.auctionId); - expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); - expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); - expect(request).to.have.property('bc').and.to.equal(1); - expect(request).to.have.property('floor').and.to.equal(null); - expect(request).to.have.property('banner').and.to.deep.equal({ sizes: [[300, 250], [300, 200]] }); - expect(request).to.have.property('gdprApplies').and.to.equal(0); - expect(request).to.have.property('consentString').and.to.equal(''); - expect(request).to.have.property('userEids').and.to.deep.equal([]); - expect(request).to.have.property('usPrivacy').and.to.equal(''); - expect(request).to.have.property('coppa').and.to.equal(0); - expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); - expect(request).to.have.property('ext').and.to.deep.equal({}); - expect(request).to.have.property('env').and.to.deep.equal({ - env: 'xe', - placement: 'test-banner' - }); - expect(request).to.have.property('device').and.to.deep.equal({ - ua: navigator.userAgent, - lang: navigator.language + it('should build basic request structure', function () { + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); + expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.auctionId); + expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); + expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); + expect(request).to.have.property('bc').and.to.equal(1); + expect(request).to.have.property('floor').and.to.equal(null); + expect(request).to.have.property('banner').and.to.deep.equal({ sizes: [[300, 250], [300, 200]] }); + expect(request).to.have.property('gdprApplies').and.to.equal(0); + expect(request).to.have.property('consentString').and.to.equal(''); + expect(request).to.have.property('userEids').and.to.deep.equal([]); + expect(request).to.have.property('usPrivacy').and.to.equal(''); + expect(request).to.have.property('coppa').and.to.equal(0); + expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); + expect(request).to.have.property('ext').and.to.deep.equal({}); + expect(request).to.have.property('env').and.to.deep.equal({ + env: 'xe', + placement: 'test-banner' + }); + expect(request).to.have.property('device').and.to.deep.equal({ + ua: navigator.userAgent, + lang: navigator.language + }); }); - }); - it('should build request with schain', function () { - const schainRequest = deepClone(defaultRequest); - schainRequest.schain = { - validation: 'strict', - config: { - ver: '1.0' - } - }; - const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; - expect(request).to.have.property('schain').and.to.deep.equal({ - validation: 'strict', - config: { - ver: '1.0' - } + it('should build request with schain', function () { + const schainRequest = deepClone(defaultRequest); + schainRequest.schain = { + validation: 'strict', + config: { + ver: '1.0' + } + }; + const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; + expect(request).to.have.property('schain').and.to.deep.equal({ + validation: 'strict', + config: { + ver: '1.0' + } + }); }); - }); - it('should build request with location', function () { - const bidderRequest = { - refererInfo: { - page: 'page', - location: 'location', - domain: 'domain', - ref: 'ref', - isAmp: false - } - }; - const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; - expect(request).to.have.property('location'); - const location = request.location; - expect(location).to.have.property('page').and.to.equal('page'); - expect(location).to.have.property('location').and.to.equal('location'); - expect(location).to.have.property('domain').and.to.equal('domain'); - expect(location).to.have.property('ref').and.to.equal('ref'); - expect(location).to.have.property('isAmp').and.to.equal(false); - }); + it('should build request with location', function () { + const bidderRequest = { + refererInfo: { + page: 'page', + location: 'location', + domain: 'domain', + ref: 'ref', + isAmp: false + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('location'); + const location = request.location; + expect(location).to.have.property('page').and.to.equal('page'); + expect(location).to.have.property('location').and.to.equal('location'); + expect(location).to.have.property('domain').and.to.equal('domain'); + expect(location).to.have.property('ref').and.to.equal('ref'); + expect(location).to.have.property('isAmp').and.to.equal(false); + }); - it('should build request with ortb2 info', function () { - const ortb2Request = deepClone(defaultRequest); - ortb2Request.ortb2 = { - site: { - name: 'name' - } - }; - const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; - expect(request).to.have.property('ortb2').and.to.deep.equal({ - site: { - name: 'name' - } + it('should build request with ortb2 info', function () { + const ortb2Request = deepClone(defaultRequest); + ortb2Request.ortb2 = { + site: { + name: 'name' + } + }; + const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; + expect(request).to.have.property('ortb2').and.to.deep.equal({ + site: { + name: 'name' + } + }); }); - }); - it('should build request with ortb2Imp info', function () { - const ortb2ImpRequest = deepClone(defaultRequest); - ortb2ImpRequest.ortb2Imp = { - ext: { - data: { - pbadslot: 'home1', - adUnitSpecificAttribute: '1' + it('should build request with ortb2Imp info', function () { + const ortb2ImpRequest = deepClone(defaultRequest); + ortb2ImpRequest.ortb2Imp = { + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } } - } - }; - const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; - expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ - ext: { - data: { - pbadslot: 'home1', - adUnitSpecificAttribute: '1' + }; + const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; + expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } } - } + }); }); - }); - it('should build request with valid bidfloor', function () { - const bfRequest = deepClone(defaultRequest); - bfRequest.getFloor = () => ({ floor: 5, currency: 'USD' }); - const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; - expect(request).to.have.property('floor').and.to.equal(5); - }); + it('should build request with valid bidfloor', function () { + const bfRequest = deepClone(defaultRequest); + bfRequest.getFloor = () => ({ floor: 5, currency: 'USD' }); + const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; + expect(request).to.have.property('floor').and.to.equal(5); + }); - it('should build request with gdpr consent data if applies', function () { - const bidderRequest = { - gdprConsent: { - gdprApplies: true, - consentString: 'qwerty' - } - }; - const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; - expect(request).to.have.property('gdprApplies').and.equals(1); - expect(request).to.have.property('consentString').and.equals('qwerty'); - }); + it('should build request with gdpr consent data if applies', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'qwerty' + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('gdprApplies').and.equals(1); + expect(request).to.have.property('consentString').and.equals('qwerty'); + }); - it('should build request with usp consent data if applies', function () { - const bidderRequest = { - uspConsent: '1YA-' - }; - const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; - expect(request).to.have.property('usPrivacy').and.equals('1YA-'); - }); + it('should build request with usp consent data if applies', function () { + const bidderRequest = { + uspConsent: '1YA-' + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('usPrivacy').and.equals('1YA-'); + }); - it('should build request with coppa 1', function () { - config.setConfig({ - coppa: true + it('should build request with coppa 1', function () { + config.setConfig({ + coppa: true + }); + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('coppa').and.equals(1); }); - const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; - expect(request).to.have.property('coppa').and.equals(1); - }); - it('should build request with extended ids', function () { - const idRequest = deepClone(defaultRequest); - idRequest.userIdAsEids = createEidsArray({ - tdid: 'TTD_ID_FROM_USER_ID_MODULE', - pubcid: 'pubCommonId_FROM_USER_ID_MODULE' - }); - const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; - expect(request).to.have.property('userEids').and.deep.equal([ - { source: 'adserver.org', uids: [ { id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: { rtiPartner: 'TDID' } } ] }, - { source: 'pubcid.org', uids: [ { id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1 } ] } - ]); - }); + it('should build request with extended ids', function () { + const idRequest = deepClone(defaultRequest); + idRequest.userIdAsEids = [ + { source: 'adserver.org', uids: [ { id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: { rtiPartner: 'TDID' } } ] }, + { source: 'pubcid.org', uids: [ { id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1 } ] } + ]; + const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; + expect(request).to.have.property('userEids').and.deep.equal(idRequest.userIdAsEids); + }); - it('should build request with video', function () { - const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; - expect(request).to.have.property('video').and.to.deep.equal({ - playerSize: [640, 480], - context: 'instream', - skipppable: true + it('should build request with video', function () { + const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; + expect(request).to.have.property('video').and.to.deep.equal({ + playerSize: [640, 480], + context: 'instream', + skipppable: true + }); + expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); }); - expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); - }); -}); - -describe('interpretResponse', function () { - it('should return empty bids', function () { - const serverResponse = { - body: { - data: null - } - }; - - const invalidResponse = spec.interpretResponse(serverResponse, {}); - expect(invalidResponse).to.be.an('array').that.is.empty; }); - it('should interpret valid response', function () { - const serverResponse = { - body: { - data: [{ - requestId: 'qwerty', - cpm: 1, - currency: 'USD', - width: 300, - height: 250, - ttl: 600, - meta: { - advertiserDomains: ['xe.works'] - }, - ext: { - pixels: [ - [ 'iframe', 'surl1' ], - [ 'image', 'surl2' ], - ] - } - }] - } - }; - - const validResponse = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequest }); - const bid = validResponse[0]; - expect(validResponse).to.be.an('array').that.is.not.empty; - expect(bid.requestId).to.equal('qwerty'); - expect(bid.cpm).to.equal(1); - expect(bid.currency).to.equal('USD'); - expect(bid.width).to.equal(300); - expect(bid.height).to.equal(250); - expect(bid.ttl).to.equal(600); - expect(bid.meta).to.deep.equal({ advertiserDomains: ['xe.works'] }); - }); + describe('interpretResponse', function () { + it('should return empty bids', function () { + const serverResponse = { + body: { + data: null + } + }; - it('should interpret valid banner response', function () { - const serverResponse = { - body: { - data: [{ - requestId: 'qwerty', - cpm: 1, - currency: 'USD', - width: 300, - height: 250, - ttl: 600, - mediaType: 'banner', - creativeId: 'xe-demo-banner', - ad: 'ad', - meta: {} - }] - } - }; - - const validResponseBanner = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequest }); - const bid = validResponseBanner[0]; - expect(validResponseBanner).to.be.an('array').that.is.not.empty; - expect(bid.mediaType).to.equal('banner'); - expect(bid.creativeId).to.equal('xe-demo-banner'); - expect(bid.ad).to.equal('ad'); - }); + const invalidResponse = spec.interpretResponse(serverResponse, {}); + expect(invalidResponse).to.be.an('array').that.is.empty; + }); - it('should interpret valid video response', function () { - const serverResponse = { - body: { - data: [{ - requestId: 'qwerty', - cpm: 1, - currency: 'USD', - width: 600, - height: 480, - ttl: 600, - mediaType: 'video', - creativeId: 'xe-demo-video', - ad: 'vast-xml', - meta: {} - }] - } - }; - - const validResponseBanner = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequestVideo }); - const bid = validResponseBanner[0]; - expect(validResponseBanner).to.be.an('array').that.is.not.empty; - expect(bid.mediaType).to.equal('video'); - expect(bid.creativeId).to.equal('xe-demo-video'); - expect(bid.ad).to.equal('vast-xml'); - }); -}); + it('should interpret valid response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + meta: { + advertiserDomains: ['xe.works'] + }, + ext: { + pixels: [ + [ 'iframe', 'surl1' ], + [ 'image', 'surl2' ], + ] + } + }] + } + }; + + const validResponse = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequest }); + const bid = validResponse[0]; + expect(validResponse).to.be.an('array').that.is.not.empty; + expect(bid.requestId).to.equal('qwerty'); + expect(bid.cpm).to.equal(1); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ttl).to.equal(600); + expect(bid.meta).to.deep.equal({ advertiserDomains: ['xe.works'] }); + }); -describe('getUserSyncs', function () { - it('shoukd handle no params', function () { - const opts = spec.getUserSyncs({}, []); - expect(opts).to.be.an('array').that.is.empty; - }); + it('should interpret valid banner response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + mediaType: 'banner', + creativeId: 'xe-demo-banner', + ad: 'ad', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequest }); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('banner'); + expect(bid.creativeId).to.equal('xe-demo-banner'); + expect(bid.ad).to.equal('ad'); + }); - it('should return empty if sync is not allowed', function () { - const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); - expect(opts).to.be.an('array').that.is.empty; + it('should interpret valid video response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 600, + height: 480, + ttl: 600, + mediaType: 'video', + creativeId: 'xe-demo-video', + ad: 'vast-xml', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequestVideo }); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('video'); + expect(bid.creativeId).to.equal('xe-demo-video'); + expect(bid.ad).to.equal('vast-xml'); + }); }); - it('should allow iframe sync', function () { - const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ - body: { - data: [{ - requestId: 'qwerty', - ext: { - pixels: [ - [ 'iframe', 'surl1?a=b' ], - [ 'image', 'surl2?a=b' ], - ] - } - }] - } - }]); - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('iframe'); - expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); - }); + describe('getUserSyncs', function () { + it('shoukd handle no params', function () { + const opts = spec.getUserSyncs({}, []); + expect(opts).to.be.an('array').that.is.empty; + }); - it('should allow pixel sync', function () { - const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ - body: { - data: [{ - requestId: 'qwerty', - ext: { - pixels: [ - [ 'iframe', 'surl1?a=b' ], - [ 'image', 'surl2?a=b' ], - ] - } - }] - } - }]); - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('image'); - expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); - }); + it('should return empty if sync is not allowed', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(opts).to.be.an('array').that.is.empty; + }); - it('should allow pixel sync and parse consent params', function () { - const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ - body: { - data: [{ - requestId: 'qwerty', - ext: { - pixels: [ - [ 'iframe', 'surl1?a=b' ], - [ 'image', 'surl2?a=b' ], - ] - } - }] - } - }], { - gdprApplies: 1, - consentString: '1YA-' - }); - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('image'); - expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); - }); -}); + it('should allow iframe sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + [ 'iframe', 'surl1?a=b' ], + [ 'image', 'surl2?a=b' ], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); -describe('getBidFloor', function () { - it('should return null when getFloor is not a function', () => { - const bid = { getFloor: 2 }; - const result = getBidFloor(bid); - expect(result).to.be.null; - }); + it('should allow pixel sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + [ 'iframe', 'surl1?a=b' ], + [ 'image', 'surl2?a=b' ], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); - it('should return null when getFloor doesnt return an object', () => { - const bid = { getFloor: () => 2 }; - const result = getBidFloor(bid); - expect(result).to.be.null; + it('should allow pixel sync and parse consent params', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + [ 'iframe', 'surl1?a=b' ], + [ 'image', 'surl2?a=b' ], + ] + } + }] + } + }], { + gdprApplies: 1, + consentString: '1YA-' + }); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); + }); }); - it('should return null when floor is not a number', () => { - const bid = { - getFloor: () => ({ floor: 'string', currency: 'USD' }) - }; - const result = getBidFloor(bid); - expect(result).to.be.null; - }); + describe('getBidFloor', function () { + it('should return null when getFloor is not a function', () => { + const bid = { getFloor: 2 }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); - it('should return null when currency is not USD', () => { - const bid = { - getFloor: () => ({ floor: 5, currency: 'EUR' }) - }; - const result = getBidFloor(bid); - expect(result).to.be.null; - }); + it('should return null when getFloor doesnt return an object', () => { + const bid = { getFloor: () => 2 }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); - it('should return floor value when everything is correct', () => { - const bid = { - getFloor: () => ({ floor: 5, currency: 'USD' }) - }; - const result = getBidFloor(bid); - expect(result).to.equal(5); + it('should return null when floor is not a number', () => { + const bid = { + getFloor: () => ({ floor: 'string', currency: 'USD' }) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when currency is not USD', () => { + const bid = { + getFloor: () => ({ floor: 5, currency: 'EUR' }) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return floor value when everything is correct', () => { + const bid = { + getFloor: () => ({ floor: 5, currency: 'USD' }) + }; + const result = getBidFloor(bid); + expect(result).to.equal(5); + }); }); -}); +}) diff --git a/test/spec/modules/yahoosspBidAdapter_spec.js b/test/spec/modules/yahoosspBidAdapter_spec.js index b0030c89371..40dc2b3c63b 100644 --- a/test/spec/modules/yahoosspBidAdapter_spec.js +++ b/test/spec/modules/yahoosspBidAdapter_spec.js @@ -3,7 +3,6 @@ import { config } from 'src/config.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; import { spec } from 'modules/yahoosspBidAdapter.js'; import {createEidsArray} from '../../../modules/userId/eids'; -import {deepClone} from '../../../src/utils'; const DEFAULT_BID_ID = '84ab500420319d'; const DEFAULT_BID_DCN = '2093845709823475'; @@ -13,18 +12,20 @@ const DEFAULT_AD_UNIT_CODE = '/19968336/header-bid-tag-1'; const DEFAULT_AD_UNIT_TYPE = 'banner'; const DEFAULT_PARAMS_BID_OVERRIDE = {}; const DEFAULT_VIDEO_CONTEXT = 'instream'; -const ADAPTER_VERSION = '1.0.2'; +const ADAPTER_VERSION = '1.1.0'; +const DEFAULT_BIDDER_CODE = 'yahooAds'; +const VALID_BIDDER_CODES = [DEFAULT_BIDDER_CODE, 'yahoossp', 'yahooAdvertising']; const PREBID_VERSION = '$prebid.version$'; const INTEGRATION_METHOD = 'prebid.js'; // Utility functions -const generateBidRequest = ({bidId, pos, adUnitCode, adUnitType, bidOverrideObject, videoContext, pubIdMode, ortb2}) => { +const generateBidRequest = ({bidderCode, bidId, pos, adUnitCode, adUnitType, bidOverrideObject, videoContext, pubIdMode, ortb2}) => { const bidRequest = { adUnitCode, auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', bidId, bidderRequestsCount: 1, - bidder: 'yahoossp', + bidder: bidderCode, bidderRequestId: '7101db09af0db2', bidderWinsCount: 0, mediaTypes: {}, @@ -79,7 +80,7 @@ let generateBidderRequest = (bidRequestArray, adUnitCode, ortb2 = {}) => { adUnitCode: adUnitCode || 'default-adUnitCode', auctionId: 'd4c83a3b-18e4-4208-b98b-63848449c7aa', auctionStart: new Date().getTime(), - bidderCode: 'yahoossp', + bidderCode: bidRequestArray[0].bidder, bidderRequestId: '112f1c7c5d399a', bids: bidRequestArray, refererInfo: { @@ -107,8 +108,9 @@ let generateBidderRequest = (bidRequestArray, adUnitCode, ortb2 = {}) => { return bidderRequest; }; -const generateBuildRequestMock = ({bidId, pos, adUnitCode, adUnitType, bidOverrideObject, videoContext, pubIdMode, ortb2}) => { +const generateBuildRequestMock = ({bidderCode, bidId, pos, adUnitCode, adUnitType, bidOverrideObject, videoContext, pubIdMode, ortb2}) => { const bidRequestConfig = { + bidderCode: bidderCode || DEFAULT_BIDDER_CODE, bidId: bidId || DEFAULT_BID_ID, pos: pos || DEFAULT_BID_POS, adUnitCode: adUnitCode || DEFAULT_AD_UNIT_CODE, @@ -174,21 +176,28 @@ const generateResponseMock = (admPayloadType, vastVersion, videoContext) => { seatbid: [{ bid: [ bidResponse ], seat: 13107 }] } }; - const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: admPayloadType, videoContext: videoContext}); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: admPayloadType, videoContext}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - return {serverResponse, data, bidderRequest}; } // Unit tests -describe('YahooSSP Bid Adapter:', () => { +describe('Yahoo Advertising Bid Adapter:', () => { + beforeEach(() => { + config.resetConfig(); + }); + describe('Validate basic properties', () => { it('should define the correct bidder code', () => { - expect(spec.code).to.equal('yahoossp') + expect(spec.code).to.equal('yahooAds'); + }); + + it('should define the correct bidder aliases', () => { + expect(spec.aliases).to.deep.equal(['yahoossp', 'yahooAdvertising']); }); it('should define the correct vendor ID', () => { - expect(spec.gvlid).to.equal(25) + expect(spec.gvlid).to.equal(25); }); }); @@ -271,33 +280,62 @@ describe('YahooSSP Bid Adapter:', () => { }); describe('user consent parameters are updated', () => { - let syncOptions = { + const syncOptions = { iframeEnabled: true, pixelEnabled: true }; - let pixelObjects = spec.getUserSyncs( - syncOptions, - SERVER_RESPONSES, - bidderRequest.gdprConsent, - bidderRequest.uspConsent, - bidderRequest.gppConsent - ); - pixelObjects.forEach(pixelObject => { - let url = pixelObject.url; - let urlParams = new URL(url).searchParams; - const expectedParams = { - 'baz': 'true', - 'gdpr_consent': bidderRequest.gdprConsent.consentString, - 'gdpr': bidderRequest.gdprConsent.gdprApplies ? '1' : '0', - 'us_privacy': bidderRequest.uspConsent, - 'gpp': bidderRequest.gppConsent.gppString, - 'gpp_sid': Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections.join(',') : '' - } - for (const [key, value] of Object.entries(expectedParams)) { - it(`Updates the ${key} consent param in user sync URL ${url}`, () => { - expect(urlParams.get(key)).to.equal(value); - }); - }; + describe('when all consent data is set', () => { + const pixelObjects = spec.getUserSyncs( + syncOptions, + SERVER_RESPONSES, + bidderRequest.gdprConsent, + bidderRequest.uspConsent, + bidderRequest.gppConsent + ); + pixelObjects.forEach(pixelObject => { + let url = pixelObject.url; + let urlParams = new URL(url).searchParams; + const expectedParams = { + 'baz': 'true', + 'gdpr_consent': bidderRequest.gdprConsent.consentString, + 'gdpr': bidderRequest.gdprConsent.gdprApplies ? '1' : '0', + 'us_privacy': bidderRequest.uspConsent, + 'gpp': bidderRequest.gppConsent.gppString, + 'gpp_sid': Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections.join(',') : '' + } + for (const [key, value] of Object.entries(expectedParams)) { + it(`Updates the ${key} consent param in user sync URL ${url}`, () => { + expect(urlParams.get(key)).to.equal(value); + }); + }; + }); + }); + + describe('when no consent data is set', () => { + const pixelObjects = spec.getUserSyncs( + syncOptions, + SERVER_RESPONSES, + undefined, + undefined, + undefined + ); + pixelObjects.forEach(pixelObject => { + let url = pixelObject.url; + let urlParams = new URL(url).searchParams; + const expectedParams = { + 'baz': 'true', + 'gdpr_consent': '', + 'gdpr': '0', + 'us_privacy': '', + 'gpp': '', + 'gpp_sid': '' + } + for (const [key, value] of Object.entries(expectedParams)) { + it(`Updates the ${key} consent param in user sync URL ${url}`, () => { + expect(urlParams.get(key)).to.equal(value); + }); + }; + }); }); }); }); @@ -647,9 +685,9 @@ describe('YahooSSP Bid Adapter:', () => { }; const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.user[param]).to.be.a('object'); - expect(data.user[param]).to.be.deep.include({[param]: {a: '123', b: '456'}}); - config.setConfig({ortb2: {}}); + const user = data.user; + expect(user[param]).to.be.a('object'); + expect(user[param]).to.be.deep.include({[param]: {a: '123', b: '456'}}); }); }); @@ -669,12 +707,14 @@ describe('YahooSSP Bid Adapter:', () => { }; const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.user.data[0][param]).to.exist; - expect(data.user.data[0][param]).to.be.a('string'); - expect(data.user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); - expect(data.site.content.data[0][param]).to.exist; - expect(data.site.content.data[0][param]).to.be.a('string'); - expect(data.site.content.data[0][param]).to.be.equal(ortb2.site.content.data[0][param]); + const user = data.user; + const site = data.site; + expect(user.data[0][param]).to.exist; + expect(user.data[0][param]).to.be.a('string'); + expect(user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); + expect(site.content.data[0][param]).to.exist; + expect(site.content.data[0][param]).to.be.a('string'); + expect(site.content.data[0][param]).to.be.equal(ortb2.site.content.data[0][param]); }); }); @@ -688,9 +728,10 @@ describe('YahooSSP Bid Adapter:', () => { }; const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.user.data[0][param]).to.exist; - expect(data.user.data[0][param]).to.be.a('array'); - expect(data.user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); + const user = data.user; + expect(user.data[0][param]).to.exist; + expect(user.data[0][param]).to.be.a('array'); + expect(user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); }); }); @@ -704,10 +745,10 @@ describe('YahooSSP Bid Adapter:', () => { }; const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.user.data[0][param]).to.exist; - expect(data.user.data[0][param]).to.be.a('object'); - expect(data.user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); - config.setConfig({ortb2: {}}); + const user = data.user; + expect(user.data[0][param]).to.exist; + expect(user.data[0][param]).to.be.a('object'); + expect(user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); }); }); @@ -815,26 +856,33 @@ describe('YahooSSP Bid Adapter:', () => { }); describe('Endpoint & Impression Request Mode:', () => { - it('should route request to config override endpoint', () => { - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const testOverrideEndpoint = 'http://foo.bar.baz.com/bidderRequest'; + afterEach(() => { config.setConfig({ - yahoossp: { - endpoint: testOverrideEndpoint + yahooAds: { + singleRequestMode: undefined } }); - const response = spec.buildRequests(validBidRequests, bidderRequest)[0]; - expect(response).to.deep.include( - { - method: 'POST', - url: testOverrideEndpoint - }); }); - it('should route request to /bidRequest endpoint when dcn & pos present', () => { - config.setConfig({ - yahoossp: {} + VALID_BIDDER_CODES.forEach(bidderCode => { + it(`should route request to config override endpoint for ${bidderCode} override config`, () => { + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode}); + const testOverrideEndpoint = 'http://foo.bar.baz.com/bidderRequest'; + const cfg = {}; + cfg[bidderCode] = { + endpoint: testOverrideEndpoint + }; + config.setConfig(cfg); + const response = spec.buildRequests(validBidRequests, bidderRequest)[0]; + expect(response).to.deep.include( + { + method: 'POST', + url: testOverrideEndpoint + }); }); + }); + + it('should route request to /bidRequest endpoint when dcn & pos present', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); const response = spec.buildRequests(validBidRequests, bidderRequest); expect(response[0]).to.deep.include({ @@ -862,15 +910,15 @@ describe('YahooSSP Bid Adapter:', () => { bidderRequest.bids = validBidRequests; config.setConfig({ - yahoossp: { + yahooAds: { singleRequestMode: true } }); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; - expect(data.imp).to.be.an('array').with.lengthOf(2); + const responsePayload = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(responsePayload.imp).to.be.an('array').with.lengthOf(2); - expect(data.imp[0]).to.deep.include({ + expect(responsePayload.imp[0]).to.deep.include({ id: DEFAULT_BID_ID, ext: { pos: DEFAULT_BID_POS, @@ -878,7 +926,7 @@ describe('YahooSSP Bid Adapter:', () => { } }); - expect(data.imp[1]).to.deep.include({ + expect(responsePayload.imp[1]).to.deep.include({ id: BID_ID_2, ext: { pos: BID_POS_2, @@ -896,8 +944,9 @@ describe('YahooSSP Bid Adapter:', () => { it('buildRequests(): should return an array with the correct amount of request objects', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const response = spec.buildRequests(validBidRequests, bidderRequest).bidderRequest; - expect(response.bids).to.be.an('array').to.have.lengthOf(1); + const reqs = spec.buildRequests(validBidRequests, bidderRequest); + expect(reqs).to.be.an('array').to.have.lengthOf(1); + expect(reqs[0]).to.be.an('object').that.has.keys('method', 'url', 'data', 'options', 'bidderRequest'); }); }); @@ -905,7 +954,7 @@ describe('YahooSSP Bid Adapter:', () => { it('should return request objects with the relevant custom headers and content type declaration', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); bidderRequest.gdprConsent.gdprApplies = false; - const options = spec.buildRequests(validBidRequests, bidderRequest).options; + const options = spec.buildRequests(validBidRequests, bidderRequest)[0].options; expect(options).to.deep.equal( { contentType: 'application/json', @@ -920,22 +969,8 @@ describe('YahooSSP Bid Adapter:', () => { describe('User data', () => { it('should set the allowed sources user eids', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - validBidRequests[0].userIdAsEids = createEidsArray({ - admixerId: 'admixerId_FROM_USER_ID_MODULE', - adtelligentId: 'adtelligentId_FROM_USER_ID_MODULE', - amxId: 'amxId_FROM_USER_ID_MODULE', - britepoolid: 'britepoolid_FROM_USER_ID_MODULE', - deepintentId: 'deepintentId_FROM_USER_ID_MODULE', - publinkId: 'publinkId_FROM_USER_ID_MODULE', - intentIqId: 'intentIqId_FROM_USER_ID_MODULE', - idl_env: 'idl_env_FROM_USER_ID_MODULE', - imuid: 'imuid_FROM_USER_ID_MODULE', - criteoId: 'criteoId_FROM_USER_ID_MODULE', - fabrickId: 'fabrickId_FROM_USER_ID_MODULE', - }); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; - - expect(data.user.ext.eids).to.deep.equal([ + validBidRequests[0].userIdAsEids = [ + {source: 'yahoo.com', uids: [{id: 'connectId_FROM_USER_ID_MODULE', atype: 3}]}, {source: 'admixer.net', uids: [{id: 'admixerId_FROM_USER_ID_MODULE', atype: 3}]}, {source: 'adtelligent.com', uids: [{id: 'adtelligentId_FROM_USER_ID_MODULE', atype: 3}]}, {source: 'amxdt.net', uids: [{id: 'amxId_FROM_USER_ID_MODULE', atype: 1}]}, @@ -947,7 +982,10 @@ describe('YahooSSP Bid Adapter:', () => { {source: 'intimatemerger.com', uids: [{id: 'imuid_FROM_USER_ID_MODULE', atype: 1}]}, {source: 'criteo.com', uids: [{id: 'criteoId_FROM_USER_ID_MODULE', atype: 1}]}, {source: 'neustar.biz', uids: [{id: 'fabrickId_FROM_USER_ID_MODULE', atype: 1}]} - ]); + ]; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + + expect(data.user.ext.eids).to.deep.equal(validBidRequests[0].userIdAsEids); }); it('should not set not allowed user eids sources', () => { @@ -955,7 +993,7 @@ describe('YahooSSP Bid Adapter:', () => { validBidRequests[0].userIdAsEids = createEidsArray({ justId: 'justId_FROM_USER_ID_MODULE' }); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.user.ext.eids).to.deep.equal([]); }); @@ -964,7 +1002,7 @@ describe('YahooSSP Bid Adapter:', () => { describe('Request Payload oRTB bid validation:', () => { it('should generate a valid openRTB bid-request object in the data field', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.site).to.deep.equal({ id: bidderRequest.bids[0].params.dcn, page: bidderRequest.refererInfo.page @@ -1013,7 +1051,7 @@ describe('YahooSSP Bid Adapter:', () => { it('should generate a valid openRTB imp.ext object in the bid-request', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); const bid = validBidRequests[0]; - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.imp[0].ext).to.deep.equal({ pos: bid.params.pos, dfp_ad_unit_code: DEFAULT_AD_UNIT_CODE @@ -1023,7 +1061,7 @@ describe('YahooSSP Bid Adapter:', () => { it('should use siteId value as site.id in the outbound bid-request when using "pubId" integration mode', () => { let { validBidRequests, bidderRequest } = generateBuildRequestMock({pubIdMode: true}); validBidRequests[0].params.siteId = '1234567'; - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.site.id).to.equal('1234567'); }); @@ -1039,7 +1077,7 @@ describe('YahooSSP Bid Adapter:', () => { } } let { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.site.publisher).to.deep.equal({ ext: { publisherblob: 'pblob', @@ -1060,7 +1098,7 @@ describe('YahooSSP Bid Adapter:', () => { } } let { validBidRequests, bidderRequest } = generateBuildRequestMock({pubIdMode: true, ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.site.publisher).to.deep.equal({ id: DEFAULT_PUBID, ext: { @@ -1073,17 +1111,13 @@ describe('YahooSSP Bid Adapter:', () => { it('should use placementId value as imp.tagid in the outbound bid-request when using "pubId" integration mode', () => { let { validBidRequests, bidderRequest } = generateBuildRequestMock({pubIdMode: true}); validBidRequests[0].params.placementId = 'header-300x250'; - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.imp[0].tagid).to.deep.equal('header-300x250'); }); }); describe('Request Payload oRTB bid.imp validation:', () => { - // Validate Banner imp imp when yahoossp.mode=undefined - it('should generate a valid "Banner" imp object', () => { - config.setConfig({ - yahoossp: {} - }); + it('should generate a valid "Banner" imp object when mode config override is undefined', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.imp[0].video).to.not.exist; @@ -1093,74 +1127,82 @@ describe('YahooSSP Bid Adapter:', () => { }); }); - // Validate Banner imp when yahoossp.mode="banner" - it('should generate a valid "Banner" imp object', () => { - config.setConfig({ - yahoossp: { mode: 'banner' } - }); - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].video).to.not.exist; - expect(data.imp[0].banner).to.deep.equal({ - mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], - format: [{w: 300, h: 250}, {w: 300, h: 600}] + // Validate Banner imp when config value for mode="banner" + VALID_BIDDER_CODES.forEach(bidderCode => { + it(`should generate a valid "Banner" imp object for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: BANNER + }; + config.setConfig(cfg); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].video).to.not.exist; + expect(data.imp[0].banner).to.deep.equal({ + mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], + format: [{w: 300, h: 250}, {w: 300, h: 600}] + }); }); - }); - // Validate Video imp - it('should generate a valid "Video" only imp object', () => { - config.setConfig({ - yahoossp: { mode: 'video' } - }); - const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: 'video'}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].banner).to.not.exist; - expect(data.imp[0].video).to.deep.equal({ - mimes: ['video/mp4', 'application/javascript'], - w: 300, - h: 250, - api: [2], - protocols: [2, 5], - startdelay: 0, - linearity: 1, - maxbitrate: undefined, - maxduration: undefined, - minduration: undefined, - delivery: undefined, - pos: undefined, - playbackmethod: undefined, - rewarded: undefined, - placement: undefined + // Validate Video imp + it(`should generate a valid "Video" only imp object for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: VIDEO + }; + config.setConfig(cfg); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode, adUnitType: 'video'}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].banner).to.not.exist; + expect(data.imp[0].video).to.deep.equal({ + mimes: ['video/mp4', 'application/javascript'], + w: 300, + h: 250, + api: [2], + protocols: [2, 5], + startdelay: 0, + linearity: 1, + maxbitrate: undefined, + maxduration: undefined, + minduration: undefined, + delivery: undefined, + pos: undefined, + playbackmethod: undefined, + rewarded: undefined, + placement: undefined + }); }); - }); - // Validate multi-format Video+banner imp - it('should generate a valid multi-format "Video + Banner" imp object', () => { - config.setConfig({ - yahoossp: { mode: 'all' } - }); - const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: 'multi-format'}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].banner).to.deep.equal({ - mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], - format: [{w: 300, h: 250}, {w: 300, h: 600}] - }); - expect(data.imp[0].video).to.deep.equal({ - mimes: ['video/mp4', 'application/javascript'], - w: 300, - h: 250, - api: [2], - protocols: [2, 5], - startdelay: 0, - linearity: 1, - maxbitrate: undefined, - maxduration: undefined, - minduration: undefined, - delivery: undefined, - pos: undefined, - playbackmethod: undefined, - rewarded: undefined, - placement: undefined + // Validate multi-format Video+banner imp + it(`should generate a valid multi-format "Video + Banner" imp object for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: 'all' + }; + config.setConfig(cfg); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode, adUnitType: 'multi-format'}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].banner).to.deep.equal({ + mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], + format: [{w: 300, h: 250}, {w: 300, h: 600}] + }); + expect(data.imp[0].video).to.deep.equal({ + mimes: ['video/mp4', 'application/javascript'], + w: 300, + h: 250, + api: [2], + protocols: [2, 5], + startdelay: 0, + linearity: 1, + maxbitrate: undefined, + maxduration: undefined, + minduration: undefined, + delivery: undefined, + pos: undefined, + playbackmethod: undefined, + rewarded: undefined, + placement: undefined + }); }); }); @@ -1179,7 +1221,6 @@ describe('YahooSSP Bid Adapter:', () => { invalidKey5: undefined }; const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].ext.kvs).to.deep.equal({ key1: 'String', key2: 123456, @@ -1192,10 +1233,6 @@ describe('YahooSSP Bid Adapter:', () => { describe('Multiple adUnit validations:', () => { // Multiple banner adUnits it('should generate multiple bid-requests for each adUnit - 2 banner only', () => { - config.setConfig({ - yahoossp: { mode: 'banner' } - }); - const BID_ID_2 = '84ab50xxxxx'; const BID_POS_2 = 'footer'; const AD_UNIT_CODE_2 = 'test-ad-unit-code-123'; @@ -1209,12 +1246,12 @@ describe('YahooSSP Bid Adapter:', () => { validBidRequests = [bidRequest, bidRequest2, bidRequest3]; bidderRequest.bids = validBidRequests; - const response = spec.buildRequests(validBidRequests, bidderRequest) - expect(response).to.be.a('array'); - expect(response.length).to.equal(2); - response.forEach((obj) => { - expect(obj.data.imp[0].video).to.not.exist - expect(obj.data.imp[0].banner).to.deep.equal({ + const reqs = spec.buildRequests(validBidRequests, bidderRequest) + expect(reqs).to.be.a('array'); + expect(reqs.length).to.equal(2); + reqs.forEach(req => { + expect(req.data.imp[0].video).to.not.exist + expect(req.data.imp[0].banner).to.deep.equal({ mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], format: [{w: 300, h: 250}, {w: 300, h: 600}] }); @@ -1223,9 +1260,11 @@ describe('YahooSSP Bid Adapter:', () => { // Multiple video adUnits it('should generate multiple bid-requests for each adUnit - 2 video only', () => { - config.setConfig({ - yahoossp: { mode: 'video' } - }); + const cfg = {}; + cfg[DEFAULT_BIDDER_CODE] = { + mode: VIDEO + }; + config.setConfig(cfg); const BID_ID_2 = '84ab50xxxxx'; const BID_POS_2 = 'footer'; const AD_UNIT_CODE_2 = 'test-ad-unit-code-123'; @@ -1233,18 +1272,18 @@ describe('YahooSSP Bid Adapter:', () => { const BID_POS_3 = 'hero'; const AD_UNIT_CODE_3 = 'video-ad-unit'; - let { bidRequest, validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: 'video'}); // video - const { bidRequest: bidRequest2 } = generateBuildRequestMock({bidId: BID_ID_2, pos: BID_POS_2, adUnitCode: AD_UNIT_CODE_2, adUnitType: 'video'}); // video - const { bidRequest: bidRequest3 } = generateBuildRequestMock({bidId: BID_ID_3, pos: BID_POS_3, adUnitCode: AD_UNIT_CODE_3}); // banner (should be filtered) + let {bidRequest, validBidRequests, bidderRequest} = generateBuildRequestMock({adUnitType: 'video'}); // video + const {bidRequest: bidRequest2} = generateBuildRequestMock({bidId: BID_ID_2, pos: BID_POS_2, adUnitCode: AD_UNIT_CODE_2, adUnitType: 'video'}); // video + const {bidRequest: bidRequest3} = generateBuildRequestMock({bidId: BID_ID_3, pos: BID_POS_3, adUnitCode: AD_UNIT_CODE_3}); // banner (should be filtered) validBidRequests = [bidRequest, bidRequest2, bidRequest3]; bidderRequest.bids = validBidRequests; - const response = spec.buildRequests(validBidRequests, bidderRequest) - expect(response).to.be.a('array'); - expect(response.length).to.equal(2); - response.forEach((obj) => { - expect(obj.data.imp[0].banner).to.not.exist - expect(obj.data.imp[0].video).to.deep.equal({ + const reqs = spec.buildRequests(validBidRequests, bidderRequest) + expect(reqs).to.be.a('array'); + expect(reqs.length).to.equal(2); + reqs.forEach(req => { + expect(req.data.imp[0].banner).to.not.exist + expect(req.data.imp[0].video).to.deep.equal({ mimes: ['video/mp4', 'application/javascript'], w: 300, h: 250, @@ -1265,9 +1304,9 @@ describe('YahooSSP Bid Adapter:', () => { }); // Mixed adUnits 1-banner, 1-video, 1-native (should filter out native) it('should generate multiple bid-requests for both "video & banner" adUnits', () => { - config.setConfig({ - yahoossp: { mode: 'all' } - }); + const cfg = {}; + cfg[DEFAULT_BIDDER_CODE] = { mode: 'all' }; + config.setConfig(cfg); const BID_ID_2 = '84ab50xxxxx'; const BID_POS_2 = 'footer'; const AD_UNIT_CODE_2 = 'video-ad-unit'; @@ -1281,21 +1320,21 @@ describe('YahooSSP Bid Adapter:', () => { validBidRequests = [bidRequest, bidRequest2, bidRequest3]; bidderRequest.bids = validBidRequests; - const response = spec.buildRequests(validBidRequests, bidderRequest); - expect(response).to.be.a('array'); - expect(response.length).to.equal(2); - response.forEach((obj) => { - expect(obj.data.imp[0].native).to.not.exist; + const reqs = spec.buildRequests(validBidRequests, bidderRequest); + expect(reqs).to.be.a('array'); + expect(reqs.length).to.equal(2); + reqs.forEach(req => { + expect(req.data.imp[0].native).to.not.exist; }); - const data1 = response[0].data; + const data1 = reqs[0].data; expect(data1.imp[0].video).to.not.exist; expect(data1.imp[0].banner).to.deep.equal({ mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], format: [{w: 300, h: 250}, {w: 300, h: 600}] }); - const data2 = response[1].data; + const data2 = reqs[1].data; expect(data2.imp[0].banner).to.not.exist; expect(data2.imp[0].video).to.deep.equal({ mimes: ['video/mp4', 'application/javascript'], @@ -1318,90 +1357,98 @@ describe('YahooSSP Bid Adapter:', () => { }); describe('Video params firstlook & bidOverride validations:', () => { - it('should first look at params.bidOverride for video placement data', () => { - config.setConfig({ - yahoossp: { mode: 'video' } - }); - const bidOverride = { - imp: { - video: { - mimes: ['video/mp4'], - w: 400, - h: 350, - api: [1], - protocols: [1, 3], - startdelay: 0, - linearity: 1, - maxbitrate: 400000, - maxduration: 3600, - minduration: 1500, - delivery: 1, - pos: 123456, - playbackmethod: 1, - rewarded: 1, - placement: 1 + VALID_BIDDER_CODES.forEach(bidderCode => { + it(`should first look at params.bidOverride for video placement data for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: VIDEO + }; + config.setConfig(cfg); + const bidOverride = { + imp: { + video: { + mimes: ['video/mp4'], + w: 400, + h: 350, + api: [1], + protocols: [1, 3], + startdelay: 0, + linearity: 1, + maxbitrate: 400000, + maxduration: 3600, + minduration: 1500, + delivery: 1, + pos: 123456, + playbackmethod: 1, + rewarded: 1, + placement: 1 + } } } - } - const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: 'video', bidOverrideObject: bidOverride}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].video).to.deep.equal(bidOverride.imp.video); - }); - - it('should second look at bid.mediaTypes.video for video placement data', () => { - config.setConfig({ - yahoossp: { mode: 'video' } - }); - let { bidRequest, bidderRequest } = generateBuildRequestMock({adUnitType: 'video'}); - bidRequest.mediaTypes.video = { - mimes: ['video/mp4'], - playerSize: [400, 350], - api: [1], - protocols: [1, 3], - startdelay: 0, - linearity: 1, - maxbitrate: 400000, - maxduration: 3600, - minduration: 1500, - delivery: 1, - pos: 123456, - playbackmethod: 1, - placement: 1 - } - const validBidRequests = [bidRequest]; - bidderRequest.bids = validBidRequests; - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].video).to.deep.equal({ - mimes: ['video/mp4'], - w: 400, - h: 350, - api: [1], - protocols: [1, 3], - startdelay: 0, - linearity: 1, - maxbitrate: 400000, - maxduration: 3600, - minduration: 1500, - delivery: 1, - pos: 123456, - playbackmethod: 1, - placement: 1, - rewarded: undefined + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode, adUnitType: 'video', bidOverrideObject: bidOverride}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].video).to.deep.equal(bidOverride.imp.video); }); - }); - it('should use params.bidOverride.device.ip override', () => { - config.setConfig({ - yahoossp: { mode: 'all' } + it(`should second look at bid.mediaTypes.video for video placement data for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: VIDEO + }; + config.setConfig(cfg); + let { bidRequest, bidderRequest } = generateBuildRequestMock({bidderCode, adUnitType: 'video'}); + bidRequest.mediaTypes.video = { + mimes: ['video/mp4'], + playerSize: [400, 350], + api: [1], + protocols: [1, 3], + startdelay: 0, + linearity: 1, + maxbitrate: 400000, + maxduration: 3600, + minduration: 1500, + delivery: 1, + pos: 123456, + playbackmethod: 1, + placement: 1 + } + const validBidRequests = [bidRequest]; + bidderRequest.bids = validBidRequests; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].video).to.deep.equal({ + mimes: ['video/mp4'], + w: 400, + h: 350, + api: [1], + protocols: [1, 3], + startdelay: 0, + linearity: 1, + maxbitrate: 400000, + maxduration: 3600, + minduration: 1500, + delivery: 1, + pos: 123456, + playbackmethod: 1, + placement: 1, + rewarded: undefined + }); }); - const bidOverride = { - device: { - ip: '1.2.3.4' + + it(`should use params.bidOverride.device.ip override for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: 'all' + }; + config.setConfig(cfg); + const bidOverride = { + device: { + ip: '1.2.3.4' + } } - } - const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: 'video', bidOverrideObject: bidOverride}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.device.ip).to.deep.equal(bidOverride.device.ip); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode, adUnitType: 'video', bidOverrideObject: bidOverride}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.device.ip).to.deep.equal(bidOverride.device.ip); + }); }); }); // #endregion buildRequests(): @@ -1417,6 +1464,22 @@ describe('YahooSSP Bid Adapter:', () => { }); describe('for mediaTypes: "video"', () => { + beforeEach(() => { + config.setConfig({ + yahooAds: { + mode: VIDEO + } + }); + }); + + afterEach(() => { + config.setConfig({ + yahooAds: { + mode: undefined + } + }); + }); + it('should insert video VPAID payload into vastXml', () => { const { serverResponse, bidderRequest } = generateResponseMock('video'); const response = spec.interpretResponse(serverResponse, {bidderRequest}); @@ -1434,28 +1497,38 @@ describe('YahooSSP Bid Adapter:', () => { expect(response[0].mediaType).to.equal('video'); }) - it('should insert video DAP O2 Player into ad', () => { - const { serverResponse, bidderRequest } = generateResponseMock('dap-o2', 'vpaid'); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ad).to.equal(''); - expect(response[0].vastUrl).to.be.undefined; - expect(response[0].vastXml).to.be.undefined; - expect(response[0].mediaType).to.equal('banner'); - }); + describe('wrapped in video players for display inventory', () => { + beforeEach(() => { + config.setConfig({ + yahooAds: { + mode: undefined + } + }); + }); - it('should insert video DAP Unified Player into ad', () => { - const { serverResponse, bidderRequest } = generateResponseMock('dap-up', 'vpaid'); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ad).to.equal(''); - expect(response[0].vastUrl).to.be.undefined; - expect(response[0].vastXml).to.be.undefined; - expect(response[0].mediaType).to.equal('banner'); - }) + it('should insert video DAP O2 Player into ad', () => { + const { serverResponse, bidderRequest } = generateResponseMock('dap-o2', 'vpaid'); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ad).to.equal(''); + expect(response[0].vastUrl).to.be.undefined; + expect(response[0].vastXml).to.be.undefined; + expect(response[0].mediaType).to.equal('banner'); + }); + + it('should insert video DAP Unified Player into ad', () => { + const { serverResponse, bidderRequest } = generateResponseMock('dap-up', 'vpaid'); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ad).to.equal(''); + expect(response[0].vastUrl).to.be.undefined; + expect(response[0].vastXml).to.be.undefined; + expect(response[0].mediaType).to.equal('banner'); + }) + }); }); describe('Support Advertiser domains', () => { it('should append bid-response adomain to meta.advertiserDomains', () => { - const { serverResponse, bidderRequest } = generateResponseMock('video', 'vpaid'); + const { serverResponse, bidderRequest } = generateResponseMock('banner'); const response = spec.interpretResponse(serverResponse, {bidderRequest}); expect(response[0].meta.advertiserDomains).to.be.a('array'); expect(response[0].meta.advertiserDomains[0]).to.equal('advertiser-domain.com'); @@ -1490,53 +1563,55 @@ describe('YahooSSP Bid Adapter:', () => { }); describe('Time To Live (ttl)', () => { - const UNSUPPORTED_TTL_FORMATS = ['string', [1, 2, 3], true, false, null, undefined]; - UNSUPPORTED_TTL_FORMATS.forEach(param => { - it('should not allow unsupported global yahoossp.ttl formats and default to 300', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - config.setConfig({ - yahoossp: { ttl: param } + VALID_BIDDER_CODES.forEach(bidderCode => { + const UNSUPPORTED_TTL_FORMATS = ['string', [1, 2, 3], true, false, null, undefined]; + UNSUPPORTED_TTL_FORMATS.forEach(param => { + it(`should not allow unsupported global ${bidderCode}.ttl formats and default to 300`, () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const cfg = {}; + cfg['yahooAds'] = { ttl: param }; + config.setConfig(cfg); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); }); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ttl).to.equal(300); - }); - it('should not allow unsupported params.ttl formats and default to 300', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - bidderRequest.bids[0].params.ttl = param; - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ttl).to.equal(300); + it('should not allow unsupported params.ttl formats and default to 300', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + bidderRequest.bids[0].params.ttl = param; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); + }); }); - }); - const UNSUPPORTED_TTL_VALUES = [-1, 3601]; - UNSUPPORTED_TTL_VALUES.forEach(param => { - it('should not allow invalid global yahoossp.ttl values 3600 < ttl < 0 and default to 300', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - config.setConfig({ - yahoossp: { ttl: param } + const UNSUPPORTED_TTL_VALUES = [-1, 3601]; + UNSUPPORTED_TTL_VALUES.forEach(param => { + it('should not allow invalid global config ttl values 3600 < ttl < 0 and default to 300', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + config.setConfig({ + yahooAds: { ttl: param } + }); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); + }); + + it('should not allow invalid params.ttl values 3600 < ttl < 0 and default to 300', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + bidderRequest.bids[0].params.ttl = param; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); }); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ttl).to.equal(300); }); - it('should not allow invalid params.ttl values 3600 < ttl < 0 and default to 300', () => { + it('should give presedence to Gloabl ttl over params.ttl ', () => { const { serverResponse, bidderRequest } = generateResponseMock('banner'); - bidderRequest.bids[0].params.ttl = param; + config.setConfig({ + yahooAds: { ttl: 500 } + }); + bidderRequest.bids[0].params.ttl = 400; const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ttl).to.equal(300); + expect(response[0].ttl).to.equal(500); }); }); - - it('should give presedence to Gloabl ttl over params.ttl ', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - config.setConfig({ - yahoossp: { ttl: 500 } - }); - bidderRequest.bids[0].params.ttl = 400; - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ttl).to.equal(500); - }); }); describe('Aliasing support', () => { diff --git a/test/spec/modules/yandexBidAdapter_spec.js b/test/spec/modules/yandexBidAdapter_spec.js index 12d413a9c93..f14e8df6c09 100644 --- a/test/spec/modules/yandexBidAdapter_spec.js +++ b/test/spec/modules/yandexBidAdapter_spec.js @@ -2,7 +2,7 @@ import { assert, expect } from 'chai'; import { spec, NATIVE_ASSETS } from 'modules/yandexBidAdapter.js'; import { parseUrl } from 'src/utils.js'; import { BANNER, NATIVE } from '../../../src/mediaTypes'; -import {OPENRTB} from '../../../modules/rtbhouseBidAdapter'; +import { config } from '../../../src/config'; describe('Yandex adapter', function () { describe('isBidRequestValid', function () { @@ -90,6 +90,57 @@ describe('Yandex adapter', function () { expect(data.site.ref).to.equal('https://ya.ru/'); }); + it('should send currency if defined', function () { + config.setConfig({ + currency: { + adServerCurrency: 'USD' + } + }); + + const bannerRequest = getBidRequest(); + const requests = spec.buildRequests([bannerRequest], bidderRequest); + const { url } = requests[0]; + const parsedRequestUrl = parseUrl(url); + const { search: query } = parsedRequestUrl + + expect(query['ssp-cur']).to.equal('USD'); + }); + + it('should send eids if defined', function() { + const bannerRequest = getBidRequest({ + userIdAsEids: [{ + source: 'sharedid.org', + uids: [ + { + id: '01', + atype: 1 + } + ] + }] + }); + + const requests = spec.buildRequests([bannerRequest], bidderRequest); + + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + + expect(request.data).to.exist; + const { data } = request; + + expect(data.user).to.exist; + expect(data.user).to.deep.equal({ + ext: { + eids: [{ + source: 'sharedid.org', + uids: [{ + id: '01', + atype: 1, + }], + }], + } + }); + }); + describe('banner', () => { it('should create valid banner object', () => { const bannerRequest = getBidRequest({ @@ -273,7 +324,7 @@ describe('Yandex adapter', function () { }, }); }); - }) + }); }); describe('interpretResponse', function () { @@ -294,6 +345,7 @@ describe('Yandex adapter', function () { 'example.com' ], adid: 'yabs.123=', + nurl: 'https://example.com/nurl/?price=${AUCTION_PRICE}&cur=${AUCTION_CURRENCY}', } ] }], @@ -314,11 +366,12 @@ describe('Yandex adapter', function () { const rtbBid = result[0]; expect(rtbBid.width).to.equal(300); expect(rtbBid.height).to.equal(250); - expect(rtbBid.cpm).to.be.within(0.1, 0.5); + expect(rtbBid.cpm).to.be.within(0.3, 0.3); expect(rtbBid.ad).to.equal(''); expect(rtbBid.currency).to.equal('USD'); expect(rtbBid.netRevenue).to.equal(true); expect(rtbBid.ttl).to.equal(180); + expect(rtbBid.nurl).to.equal('https://example.com/nurl/?price=0.3&cur=USD'); expect(rtbBid.meta.advertiserDomains).to.deep.equal(['example.com']); }); diff --git a/test/spec/modules/yieldloveBidAdapter_spec.js b/test/spec/modules/yieldloveBidAdapter_spec.js new file mode 100644 index 00000000000..b142eef0ffa --- /dev/null +++ b/test/spec/modules/yieldloveBidAdapter_spec.js @@ -0,0 +1,128 @@ +import { expect } from 'chai'; +import { spec } from 'modules/yieldloveBidAdapter.js'; + +const ENDPOINT_URL = 'https://s2s.yieldlove-ad-serving.net/openrtb2/auction'; + +// test params +const pid = 34437; +const rid = 'website.com'; + +describe('Yieldlove Bid Adaper', function () { + const bidRequests = [ + { + 'bidder': 'yieldlove', + 'adUnitCode': 'adunit-code', + 'sizes': [ [300, 250] ], + 'params': { + pid, + rid + } + } + ]; + + const serverResponse = { + body: { + seatbid: [ + { + bid: [ + { + impid: 'aaaa', + price: 0.5, + w: 300, + h: 250, + adm: '
test
', + crid: '1234', + } + ] + } + ], + ext: {} + } + } + + describe('isBidRequestValid', () => { + const bid = bidRequests[0]; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not present', function () { + const invalidBid = { ...bid, params: {} }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when required param "pid" is not present', function () { + const invalidBid = { ...bid, params: { ...bid.params, pid: undefined } }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when required param "rid" is not present', function () { + const invalidBid = { ...bid, params: { ...bid.params, rid: undefined } }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + it('should build the request', function () { + const request = spec.buildRequests(bidRequests, {}); + const payload = request.data; + const url = request.url; + + expect(url).to.equal(ENDPOINT_URL); + + expect(payload.site).to.exist; + expect(payload.site.publisher).to.exist; + expect(payload.site.publisher.id).to.exist; + expect(payload.site.publisher.id).to.equal(rid); + expect(payload.site.domain).to.exist; + expect(payload.site.domain).to.equal(rid); + + expect(payload.imp).to.exist; + expect(payload.imp[0]).to.exist; + expect(payload.imp[0].ext).to.exist; + expect(payload.imp[0].ext.prebid).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest.id).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest.id).to.equal(pid.toString()); + }); + }); + + describe('interpretResponse', () => { + it('should interpret the response by pushing it in the bids elem', function () { + const allResponses = spec.interpretResponse(serverResponse); + const response = allResponses[0]; + const seatbid = serverResponse.body.seatbid[0].bid[0]; + + expect(response.requestId).to.exist; + expect(response.requestId).to.equal(seatbid.impid); + expect(response.cpm).to.exist; + expect(response.cpm).to.equal(seatbid.price); + expect(response.width).to.exist; + expect(response.width).to.equal(seatbid.w); + expect(response.height).to.exist; + expect(response.height).to.equal(seatbid.h); + expect(response.ad).to.exist; + expect(response.ad).to.equal(seatbid.adm); + expect(response.ttl).to.exist; + expect(response.creativeId).to.exist; + expect(response.creativeId).to.equal(seatbid.crid); + expect(response.netRevenue).to.exist; + expect(response.currency).to.exist; + }); + }); + + describe('getUserSyncs', function() { + it('should retrieve user iframe syncs', function () { + expect(spec.getUserSyncs({ iframeEnabled: true }, [serverResponse], undefined, undefined)).to.deep.equal([{ + type: 'iframe', + url: 'https://cdn-a.yieldlove.com/load-cookie.html?endpoint=yieldlove&max_sync_count=100&gdpr=NaN&gdpr_consent=&' + }]); + + expect(spec.getUserSyncs({ iframeEnabled: true }, [serverResponse], { gdprApplies: true, consentString: 'example' }, undefined)).to.deep.equal([{ + type: 'iframe', + url: 'https://cdn-a.yieldlove.com/load-cookie.html?endpoint=yieldlove&max_sync_count=100&gdpr=1&gdpr_consent=example&' + }]); + }); + }); +}) diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 3706f770da8..a80d0a842b7 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -74,7 +74,6 @@ describe('YieldmoAdapter', function () { bidderRequestId: '14c4ede8c693f', bids, auctionStart: 1520001292880, - timeout: 3000, start: 1520001292884, doneCbCallCount: 0, refererInfo: { @@ -169,6 +168,14 @@ describe('YieldmoAdapter', function () { expect(requests[0].url).to.be.equal(BANNER_ENDPOINT); }); + it('should pass default timeout in bid request', function () { + const requests = build([mockBannerBid()]); + expect(requests[0].data.tmax).to.equal(400); + }); + it('should pass tmax to bid request', function () { + const requests = build([mockBannerBid()], mockBidderRequest({timeout: 1000})); + expect(requests[0].data.tmax).to.equal(1000); + }); it('should not blow up if crumbs is undefined', function () { expect(function () { build([mockBannerBid({crumbs: undefined})]); @@ -425,6 +432,18 @@ describe('YieldmoAdapter', function () { expect(requests[0].url).to.be.equal(VIDEO_ENDPOINT); }); + it('should not require params.video if required props in mediaTypes.video', function () { + videoBid.mediaTypes.video = { + ...videoBid.mediaTypes.video, + ...videoBid.params.video + }; + delete videoBid.params.video; + const requests = build([videoBid]); + expect(requests.length).to.equal(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.be.equal(VIDEO_ENDPOINT); + }); + it('should add mediaTypes.video prop to the imp.video prop', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['minduration'] = 40; expect(buildVideoBidAndGetVideoParam().minduration).to.equal(40); @@ -447,6 +466,20 @@ describe('YieldmoAdapter', function () { expect(buildVideoBidAndGetVideoParam().mimes).to.deep.equal(['video/mkv']); }); + it('should validate protocol in video bid request', function () { + expect( + spec.isBidRequestValid( + mockVideoBid({}, {}, { protocols: [2, 3, 11] }) + ) + ).to.be.true; + + expect( + spec.isBidRequestValid( + mockVideoBid({}, {}, { protocols: [2, 3, 10] }) + ) + ).to.be.false; + }); + describe('video.skip state check', () => { it('should not set video.skip if neither *.video.skip nor *.video.skippable is present', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['skippable'] = false; diff --git a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js index 15a1155f378..5194a6a526a 100644 --- a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js @@ -1,20 +1,21 @@ import zetaAnalyticsAdapter from 'modules/zeta_global_sspAnalyticsAdapter.js'; import {config} from 'src/config'; import CONSTANTS from 'src/constants.json'; +import {server} from '../../mocks/xhr.js'; import {logError} from '../../../src/utils'; let utils = require('src/utils'); let events = require('src/events'); -const MOCK = { - STUB: { - 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' - }, +const EVENTS = { AUCTION_END: { 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', 'timestamp': 1638441234544, 'auctionEnd': 1638441234784, 'auctionStatus': 'completed', + 'metrics': { + 'someMetric': 1 + }, 'adUnits': [ { 'code': '/19968336/header-bid-tag-0', @@ -74,13 +75,6 @@ const MOCK = { 'bids': [ { 'bidder': 'zeta_global_ssp', - 'params': { - 'sid': 111, - 'tags': { - 'shortname': 'prebid_analytics_event_test_shortname', - 'position': 'test_position' - } - }, 'mediaTypes': { 'banner': { 'sizes': [ @@ -309,6 +303,9 @@ const MOCK = { 'cpm': 2.258302852806723, 'currency': 'USD', 'ad': 'test_ad', + 'metrics': { + 'someMetric': 0 + }, 'ttl': 200, 'creativeId': '456456456', 'netRevenue': true, @@ -344,11 +341,7 @@ const MOCK = { 'status': 'rendered', 'params': [ { - 'sid': 111, - 'tags': { - 'shortname': 'prebid_analytics_event_test_shortname', - 'position': 'test_position' - } + 'nonZetaParam': 'nonZetaValue' } ] }, @@ -358,14 +351,11 @@ const MOCK = { describe('Zeta Global SSP Analytics Adapter', function() { let sandbox; - let xhr; let requests; beforeEach(function() { sandbox = sinon.sandbox.create(); - requests = []; - xhr = sandbox.useFakeXMLHttpRequest(); - xhr.onCreate = request => requests.push(request); + requests = server.requests; sandbox.stub(events, 'getEvents').returns([]); }); @@ -395,33 +385,45 @@ describe('Zeta Global SSP Analytics Adapter', function() { zetaAnalyticsAdapter.disableAnalytics(); }); - it('events are sent', function() { - this.timeout(5000); - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AUCTION_END, MOCK.AUCTION_END); - events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_RESPONSE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.NO_BID, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_WON, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BIDDER_DONE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BIDDER_ERROR, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.SET_TARGETING, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.REQUEST_BIDS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.ADD_AD_UNITS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AD_RENDER_FAILED, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED); - events.emit(CONSTANTS.EVENTS.TCF2_ENFORCEMENT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AUCTION_DEBUG, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_VIEWABLE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.STALE_RENDER, MOCK.STUB); + it('Move ZetaParams through analytics events', function() { + this.timeout(3000); + + events.emit(CONSTANTS.EVENTS.AUCTION_END, EVENTS.AUCTION_END); + events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, EVENTS.AD_RENDER_SUCCEEDED); + + expect(requests.length).to.equal(2); + const auctionEnd = JSON.parse(requests[0].requestBody); + const auctionSucceeded = JSON.parse(requests[1].requestBody); + + expect(auctionSucceeded.bid.params[0]).to.be.deep.equal(EVENTS.AUCTION_END.adUnits[0].bids[0].params); + expect(EVENTS.AUCTION_END.adUnits[0].bids[0].bidder).to.be.equal('zeta_global_ssp'); + }); + + it('Keep only needed fields', function() { + this.timeout(3000); + + events.emit(CONSTANTS.EVENTS.AUCTION_END, EVENTS.AUCTION_END); + events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, EVENTS.AD_RENDER_SUCCEEDED); expect(requests.length).to.equal(2); - expect(JSON.parse(requests[0].requestBody)).to.deep.equal(MOCK.AUCTION_END); - expect(JSON.parse(requests[1].requestBody)).to.deep.equal(MOCK.AD_RENDER_SUCCEEDED); + const auctionEnd = JSON.parse(requests[0].requestBody); + const auctionSucceeded = JSON.parse(requests[1].requestBody); + + expect(auctionEnd.adUnitCodes[0]).to.be.equal('/19968336/header-bid-tag-0'); + expect(auctionEnd.adUnits[0].bids[0].bidder).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.auctionEnd).to.be.equal(1638441234784); + expect(auctionEnd.auctionId).to.be.equal('75e394d9-ccce-4978-9238-91e6a1ac88a1'); + expect(auctionEnd.bidderRequests[0].bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.bidsReceived[0].bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.noBids[0].bidder).to.be.equal('appnexus'); + + expect(auctionSucceeded.adId).to.be.equal('5759bb3ef7be1e8'); + expect(auctionSucceeded.bid.auctionId).to.be.equal('75e394d9-ccce-4978-9238-91e6a1ac88a1'); + expect(auctionSucceeded.bid.requestId).to.be.equal('206be9a13236af'); + expect(auctionSucceeded.bid.bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionSucceeded.bid.creativeId).to.be.equal('456456456'); + expect(auctionSucceeded.bid.size).to.be.equal('480x320'); + expect(auctionSucceeded.doc.location.hostname).to.be.equal('localhost'); }); }); }); diff --git a/test/spec/modules/zeta_global_sspBidAdapter_spec.js b/test/spec/modules/zeta_global_sspBidAdapter_spec.js index 601f4546a29..d4fe28eff90 100644 --- a/test/spec/modules/zeta_global_sspBidAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspBidAdapter_spec.js @@ -1,5 +1,6 @@ import {spec} from '../../../modules/zeta_global_sspBidAdapter.js' import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {deepClone} from '../../../src/utils'; describe('Zeta Ssp Bid Adapter', function () { const eids = [ @@ -50,7 +51,6 @@ describe('Zeta Ssp Bid Adapter', function () { someTag: 444, }, sid: 'publisherId', - shortname: 'test_shortname', tagid: 'test_tag_id', site: { page: 'testPage' @@ -253,11 +253,13 @@ describe('Zeta Ssp Bid Adapter', function () { }; it('Test the bid validation function', function () { - const validBid = spec.isBidRequestValid(bannerRequest[0]); - const invalidBid = spec.isBidRequestValid(null); + const invalidBid = deepClone(bannerRequest[0]); + invalidBid.params = {}; + const isValidBid = spec.isBidRequestValid(bannerRequest[0]); + const isInvalidBid = spec.isBidRequestValid(null); - expect(validBid).to.be.true; - expect(invalidBid).to.be.false; + expect(isValidBid).to.be.true; + expect(isInvalidBid).to.be.false; }); it('Test provide eids', function () { @@ -453,7 +455,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test required params in banner request', function () { const request = spec.buildRequests(bannerRequest, bannerRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.ext.sid).to.eql('publisherId'); expect(payload.ext.tags.someTag).to.eql(444); expect(payload.ext.tags.shortname).to.be.undefined; @@ -462,7 +464,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test required params in video request', function () { const request = spec.buildRequests(videoRequest, videoRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.ext.sid).to.eql('publisherId'); expect(payload.ext.tags.someTag).to.eql(444); expect(payload.ext.tags.shortname).to.be.undefined; @@ -471,7 +473,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test multi imp', function () { const request = spec.buildRequests(multiImpRequest, multiImpRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.imp.length).to.eql(2); diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index dee177d4b9b..9cfee6f5cd8 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -631,7 +631,8 @@ describe('native.js', function () { eventtrackers: [ { event: 1, method: 1, url: 'https://sampleurl.com' }, { event: 1, method: 2, url: 'https://sampleurljs.com' } - ] + ], + imptrackers: [ 'https://sample-imp.com' ] } describe('toLegacyResponse', () => { it('returns assets in legacy format for ortb responses', () => { @@ -640,8 +641,9 @@ describe('native.js', function () { expect(actual.title).to.equal('vtitle'); expect(actual.clickUrl).to.equal('url'); expect(actual.javascriptTrackers).to.equal(''); - expect(actual.impressionTrackers.length).to.equal(1); - expect(actual.impressionTrackers[0]).to.equal('https://sampleurl.com'); + expect(actual.impressionTrackers.length).to.equal(2); + expect(actual.impressionTrackers).to.contain('https://sampleurl.com'); + expect(actual.impressionTrackers).to.contain('https://sample-imp.com'); }); }); }); @@ -860,6 +862,9 @@ describe('validate native', function () { }] }, address: {}, + privacyLink: { + required: true + } }, }, }; @@ -915,6 +920,7 @@ describe('validate native', function () { type: 9, } }); + expect(ortb.privacy).to.equal(1); }); ['bogusKey', 'clickUrl', 'privacyLink'].forEach(nativeKey => { @@ -1022,11 +1028,14 @@ describe('validate native', function () { expect(oldNativeRequest.sponsoredBy).to.include({ required: true, len: 25 - }) + }); expect(oldNativeRequest.body).to.include({ required: true, len: 140 - }) + }); + expect(oldNativeRequest.privacyLink).to.include({ + required: false + }); }); if (FEATURES.NATIVE) { @@ -1197,6 +1206,12 @@ describe('legacyPropertiesToOrtbNative', () => { expect(native.jstracker).to.eql('some-markupsome-other-markup'); }) }); + describe('privacylink', () => { + it('should convert privacyLink to privacy', () => { + const native = legacyPropertiesToOrtbNative({privacyLink: 'https:/my-privacy-link.com'}); + expect(native.privacy).to.eql('https:/my-privacy-link.com'); + }) + }) }); describe('fireImpressionTrackers', () => { diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index cff26df2e4d..98d841d9c7c 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -965,13 +965,23 @@ describe('adapterManager tests', function () { }]; it('invokes callBids on the S2S adapter', function () { + const done = sinon.stub(); + const onTimelyResponse = sinon.stub(); + prebidServerAdapterMock.callBids.callsFake((_1, _2, _3, done) => { + done(); + }); adapterManager.callBids( getAdUnits(), bidRequests, () => {}, - () => () => {} + done, + undefined, + undefined, + onTimelyResponse ); sinon.assert.calledTwice(prebidServerAdapterMock.callBids); + sinon.assert.calledTwice(done); + bidRequests.forEach(br => sinon.assert.calledWith(onTimelyResponse, br.bidderRequestId)); }); // Enable this test when prebidServer adapter is made 1.0 compliant diff --git a/test/spec/unit/core/ajax_spec.js b/test/spec/unit/core/ajax_spec.js new file mode 100644 index 00000000000..a3a0459b980 --- /dev/null +++ b/test/spec/unit/core/ajax_spec.js @@ -0,0 +1,415 @@ +import {attachCallbacks, dep, fetcherFactory, toFetchRequest} from '../../../../src/ajax.js'; +import {config} from 'src/config.js'; +import {server} from '../../../mocks/xhr.js'; +import * as utils from 'src/utils.js'; +import {logError} from 'src/utils.js'; + +const EXAMPLE_URL = 'https://www.example.com'; + +describe('fetcherFactory', () => { + let clock; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + server.autoTimeout = true; + }); + + afterEach(() => { + clock.runAll(); + clock.restore(); + config.resetConfig(); + }); + + Object.entries({ + 'URL': EXAMPLE_URL, + 'request object': new Request(EXAMPLE_URL) + }).forEach(([t, resource]) => { + it(`times out after timeout when fetching ${t}`, (done) => { + const fetch = fetcherFactory(1000); + const resp = fetch(resource); + clock.tick(900); + expect(server.requests[0].fetch.request.signal.aborted).to.be.false; + clock.tick(100); + expect(server.requests[0].fetch.request.signal.aborted).to.be.true; + resp.catch(() => done()); + }); + }); + + it('does not timeout after it completes', () => { + const fetch = fetcherFactory(1000); + const resp = fetch(EXAMPLE_URL); + server.requests[0].respond(); + return resp.then(() => { + clock.tick(2000); + expect(server.requests[0].fetch.request.signal.aborted).to.be.false; + }); + }); + + Object.entries({ + 'disableAjaxTimeout is set'() { + const fetcher = fetcherFactory(1000); + config.setConfig({disableAjaxTimeout: true}); + return fetcher; + }, + 'timeout is null'() { + return fetcherFactory(null); + }, + }).forEach(([t, mkFetcher]) => { + it(`does not timeout if ${t}`, (done) => { + const fetch = mkFetcher(); + const pm = fetch(EXAMPLE_URL); + clock.tick(2000); + server.requests[0].respond(); + pm.then(() => done()); + }); + }); + + Object.entries({ + 'local URL': ['/local.html', window.origin], + 'remote URL': [EXAMPLE_URL + '/remote.html', EXAMPLE_URL], + 'request with local URL': [new Request('/local.html'), window.origin], + 'request with remote URL': [new Request(EXAMPLE_URL + '/remote.html'), EXAMPLE_URL] + }).forEach(([t, [resource, expectedOrigin]]) => { + describe(`using ${t}`, () => { + it('calls request, passing origin', () => { + const request = sinon.stub(); + const fetch = fetcherFactory(1000, {request}); + fetch(resource); + sinon.assert.calledWith(request, expectedOrigin); + }); + + Object.entries({ + success: 'respond', + error: 'error' + }).forEach(([t, method]) => { + it(`calls done on ${t}, passing origin`, () => { + const done = sinon.stub(); + const fetch = fetcherFactory(1000, {done}); + const req = fetch(resource).catch(() => null).then(() => { + sinon.assert.calledWith(done, expectedOrigin); + }); + server.requests[0][method](); + return req; + }); + }); + }); + }); +}); + +describe('toFetchRequest', () => { + Object.entries({ + 'simple POST': { + url: EXAMPLE_URL, + data: 'data', + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'POST', + }, + text: 'data', + headers: { + 'content-type': 'text/plain' + } + } + }, + 'POST with headers': { + url: EXAMPLE_URL, + data: '{"json": "body"}', + options: { + contentType: 'application/json', + customHeaders: { + 'x-custom': 'value' + } + }, + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'POST', + }, + text: '{"json": "body"}', + headers: { + 'content-type': 'application/json', + 'X-Custom': 'value' + } + } + }, + 'simple GET': { + url: EXAMPLE_URL, + data: {p1: 'v1', p2: 'v2'}, + options: { + method: 'GET', + }, + expect: { + request: { + url: EXAMPLE_URL + '/?p1=v1&p2=v2', + method: 'GET' + }, + text: '', + headers: { + 'content-type': 'text/plain' + } + } + }, + 'GET with credentials': { + url: EXAMPLE_URL, + data: null, + options: { + method: 'GET', + withCredentials: true, + }, + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'GET', + credentials: 'include' + }, + text: '', + headers: { + 'content-type': 'text/plain' + } + } + } + }).forEach(([t, {url, data, options, expect: {request, text, headers}}]) => { + it(`can build ${t}`, () => { + const req = toFetchRequest(url, data, options); + return req.text().then(body => { + Object.entries(request).forEach(([prop, val]) => { + expect(req[prop]).to.eql(val); + }); + const hdr = new Headers(headers); + Array.from(req.headers.entries()).forEach(([name, val]) => { + expect(hdr.get(name)).to.eql(val); + }); + expect(body).to.eql(text); + }); + }); + }); + + describe('browsingTopics', () => { + Object.entries({ + 'browsingTopics = true': [{browsingTopics: true}, true], + 'browsingTopics = false': [{browsingTopics: false}, false], + 'browsingTopics is undef': [{}, false] + }).forEach(([t, [opts, shouldBeSet]]) => { + describe(`when options has ${t}`, () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore(); + }); + + it(`should ${!shouldBeSet ? 'not ' : ''}be set when in a secure context`, () => { + sandbox.stub(window, 'isSecureContext').get(() => true); + toFetchRequest(EXAMPLE_URL, null, opts); + sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: shouldBeSet ? true : undefined}); + }); + it(`should not be set when not in a secure context`, () => { + sandbox.stub(window, 'isSecureContext').get(() => false); + toFetchRequest(EXAMPLE_URL, null, opts); + sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: undefined}); + }); + }) + }) + }) +}); + +describe('attachCallbacks', () => { + const sampleHeaders = new Headers({ + 'x-1': 'v1', + 'x-2': 'v2' + }); + + function responseFactory(body, props) { + props = Object.assign({headers: sampleHeaders, url: EXAMPLE_URL}, props); + return function () { + return { + response: Object.defineProperties(new Response(body, props), { + url: { + get: () => props.url + } + }), + body: body || '' + }; + }; + } + + function expectNullXHR(response) { + return new Promise((resolve, reject) => { + attachCallbacks(Promise.resolve(response), { + success: () => { + reject(new Error('should not succeed')); + }, + error(statusText, xhr) { + expect(statusText).to.eql(''); + sinon.assert.match(xhr, { + readyState: XMLHttpRequest.DONE, + status: 0, + statusText: '', + responseText: '', + response: '', + responseXML: null + }); + expect(xhr.getResponseHeader('any')).to.be.null; + resolve(); + } + }); + }); + } + + it('runs error callback on rejections', () => { + return expectNullXHR(Promise.reject(new Error())); + }); + + Object.entries({ + '2xx response': { + success: true, + makeResponse: responseFactory('body', {status: 200, statusText: 'OK'}) + }, + '2xx response with no body': { + success: true, + makeResponse: responseFactory(null, {status: 204, statusText: 'No content'}) + }, + '2xx response with XML': { + success: true, + xml: true, + makeResponse: responseFactory('', { + status: 200, + statusText: 'OK', + headers: {'content-type': 'application/xml;charset=UTF8'} + }) + }, + '2xx response with HTML': { + success: true, + xml: true, + makeResponse: responseFactory('

', { + status: 200, + statusText: 'OK', + headers: {'content-type': 'text/html;charset=UTF-8'} + }) + }, + '304 response': { + success: true, + makeResponse: responseFactory(null, {status: 304, statusText: 'Moved permanently'}) + }, + '4xx response': { + success: false, + makeResponse: responseFactory('body', {status: 400, statusText: 'Invalid request'}) + }, + '5xx response': { + success: false, + makeResponse: responseFactory('body', {status: 503, statusText: 'Gateway error'}) + }, + '4xx response with XML': { + success: false, + xml: true, + makeResponse: responseFactory('', { + status: 404, + statusText: 'Not found', + headers: { + 'content-type': 'application/xml' + } + }) + } + }).forEach(([t, {success, makeResponse, xml}]) => { + const cbType = success ? 'success' : 'error'; + + describe(`for ${t}`, () => { + let sandbox, response, body; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.spy(utils, 'logError'); + ({response, body} = makeResponse()); + }); + + afterEach(() => { + sandbox.restore(); + }) + + function checkXHR(xhr) { + utils.logError.resetHistory(); + const serialized = JSON.parse(JSON.stringify(xhr)) + // serialization of `responseXML` should not generate console messages + sinon.assert.notCalled(utils.logError); + + sinon.assert.match(serialized, { + readyState: XMLHttpRequest.DONE, + status: response.status, + statusText: response.statusText, + responseType: '', + responseURL: response.url, + response: body, + responseText: body, + }); + if (xml) { + expect(xhr.responseXML.querySelectorAll('*').length > 0).to.be.true; + } else { + expect(serialized.responseXML).to.not.exist; + } + Array.from(response.headers.entries()).forEach(([name, value]) => { + expect(xhr.getResponseHeader(name)).to.eql(value); + }); + expect(xhr.getResponseHeader('$$missing-header')).to.be.null; + } + + it(`runs ${cbType} callback`, (done) => { + attachCallbacks(Promise.resolve(response), { + success(payload, xhr) { + expect(success).to.be.true; + expect(payload).to.eql(body); + checkXHR(xhr); + done(); + }, + error(statusText, xhr) { + expect(success).to.be.false; + expect(statusText).to.eql(response.statusText); + checkXHR(xhr); + done(); + } + }); + }); + + it(`runs error callback if body cannot be retrieved`, () => { + response.text = () => Promise.reject(new Error()); + return expectNullXHR(response); + }); + + if (success) { + it('accepts a single function as success callback', (done) => { + attachCallbacks(Promise.resolve(response), function (payload, xhr) { + expect(payload).to.eql(body); + checkXHR(xhr); + done(); + }) + }) + } + }); + }); + + describe('callback exceptions', () => { + Object.entries({ + success: responseFactory(null, {status: 204}), + error: responseFactory('', {status: 400}), + }).forEach(([cbType, makeResponse]) => { + it(`do not choke ${cbType} callbacks`, () => { + const {response} = makeResponse(); + return new Promise((resolve) => { + const result = {success: false, error: false}; + attachCallbacks(Promise.resolve(response), { + success() { + result.success = true; + throw new Error(); + }, + error() { + result.error = true; + throw new Error(); + } + }); + setTimeout(() => resolve(result), 20); + }).then(result => { + Object.entries(result).forEach(([typ, ran]) => { + expect(ran).to.be[typ === cbType ? 'true' : 'false'] + }) + }); + }); + }); + }); +}); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 751c90af50f..f089059b65a 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -13,9 +13,8 @@ import {stubAuctionIndex} from '../../../helpers/indexStub.js'; import {bidderSettings} from '../../../../src/bidderSettings.js'; import {decorateAdUnitsWithNativeParams} from '../../../../src/native.js'; import * as activityRules from 'src/activities/rules.js'; -import {sandbox} from 'sinon'; import {MODULE_TYPE_BIDDER} from '../../../../src/activities/modules.js'; -import {ACTIVITY_TRANSMIT_TID} from '../../../../src/activities/activities.js'; +import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../../../../src/activities/activities.js'; const CODE = 'sampleBidder'; const MOCK_BIDS_REQUEST = { @@ -49,123 +48,102 @@ before(() => { let wrappedCallback = config.callbackWithBidder(CODE); -describe('bidders created by newBidder', function () { - let spec; - let bidder; - let addBidResponseStub; - let doneStub; - - beforeEach(function () { - spec = { - code: CODE, - isBidRequestValid: sinon.stub(), - buildRequests: sinon.stub(), - interpretResponse: sinon.stub(), - getUserSyncs: sinon.stub() - }; - - addBidResponseStub = sinon.stub(); - addBidResponseStub.reject = sinon.stub(); - doneStub = sinon.stub(); - }); - - describe('when the ajax response is irrelevant', function () { - let sandbox; - let ajaxStub; - let getConfigSpy; - let aliasRegistryStub, aliasRegistry; +describe('bidderFactory', () => { + describe('bidders created by newBidder', function () { + let spec; + let bidder; + let addBidResponseStub; + let doneStub; beforeEach(function () { - sandbox = sinon.sandbox.create(); - sandbox.stub(activityRules, 'isActivityAllowed').callsFake(() => true); - ajaxStub = sandbox.stub(ajax, 'ajax'); - addBidResponseStub.reset(); - getConfigSpy = sandbox.spy(config, 'getConfig'); - doneStub.reset(); - aliasRegistry = {}; - aliasRegistryStub = sandbox.stub(adapterManager, 'aliasRegistry'); - aliasRegistryStub.get(() => aliasRegistry); - }); + spec = { + code: CODE, + isBidRequestValid: sinon.stub(), + buildRequests: sinon.stub(), + interpretResponse: sinon.stub(), + getUserSyncs: sinon.stub() + }; - afterEach(function () { - sandbox.restore(); + addBidResponseStub = sinon.stub(); + addBidResponseStub.reject = sinon.stub(); + doneStub = sinon.stub(); }); - it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: true - } - }); - spec.code = 'fakeBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); + describe('when the ajax response is irrelevant', function () { + let sandbox; + let ajaxStub; + let getConfigSpy; + let aliasRegistryStub, aliasRegistry; - it('should let registerSyncs run with valid alias and aliasSync enabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: true - } + beforeEach(function () { + sandbox = sinon.sandbox.create(); + sandbox.stub(activityRules, 'isActivityAllowed').callsFake(() => true); + ajaxStub = sandbox.stub(ajax, 'ajax'); + addBidResponseStub.reset(); + getConfigSpy = sandbox.spy(config, 'getConfig'); + doneStub.reset(); + aliasRegistry = {}; + aliasRegistryStub = sandbox.stub(adapterManager, 'aliasRegistry'); + aliasRegistryStub.get(() => aliasRegistry); }); - spec.code = 'aliasBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); - it('should let registerSyncs run with invalid alias and aliasSync disabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: false - } + afterEach(function () { + sandbox.restore(); }); - spec.code = 'fakeBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); - it('should not let registerSyncs run with valid alias and aliasSync disabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: false - } + it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); }); - spec.code = 'aliasBidder'; - const bidder = newBidder(spec); - aliasRegistry = {[spec.code]: CODE}; - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(false); - }); - describe('transaction IDs', () => { - Object.entries({ - 'be hidden': false, - 'not be hidden': true, - }).forEach(([t, allowed]) => { - const expectation = allowed ? (val) => expect(val).to.exist : (val) => expect(val).to.not.exist; + it('should let registerSyncs run with valid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); - function checkBidRequest(br) { - ['auctionId', 'transactionId'].forEach((prop) => expectation(br[prop])); - } + it('should let registerSyncs run with invalid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); - function checkBidderRequest(br) { - expectation(br.auctionId); - br.bids.forEach(checkBidRequest); - } + it('should not let registerSyncs run with valid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false + } + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + aliasRegistry = {[spec.code]: CODE}; + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(false); + }); - it(`should ${t} from the spec logic when the transmitTid activity is${allowed ? '' : ' not'} allowed`, () => { - spec.isBidRequestValid.callsFake(br => { - checkBidRequest(br); - return true; - }); - spec.buildRequests.callsFake((bidReqs, bidderReq) => { - checkBidderRequest(bidderReq); - bidReqs.forEach(checkBidRequest); - return {method: 'POST'}; - }); + describe('transaction IDs', () => { + beforeEach(() => { + activityRules.isActivityAllowed.reset(); + ajaxStub.callsFake((_, callback) => callback.success(null, {getResponseHeader: sinon.stub()})); spec.interpretResponse.callsFake(() => [ { requestId: 'bid', @@ -176,599 +154,876 @@ describe('bidders created by newBidder', function () { currency: 'USD' } ]) - activityRules.isActivityAllowed.reset(); - activityRules.isActivityAllowed.callsFake(() => allowed); + }); - ajaxStub.callsFake((_, callback) => callback.success(null, {getResponseHeader: sinon.stub()})); + Object.entries({ + 'be hidden': false, + 'not be hidden': true, + }).forEach(([t, allowed]) => { + const expectation = allowed ? (val) => expect(val).to.exist : (val) => expect(val).to.not.exist; - const bidder = newBidder(spec); + function checkBidRequest(br) { + ['auctionId', 'transactionId'].forEach((prop) => expectation(br[prop])); + } + + function checkBidderRequest(br) { + expectation(br.auctionId); + br.bids.forEach(checkBidRequest); + } - bidder.callBids({ + it(`should ${t} from the spec logic when the transmitTid activity is${allowed ? '' : ' not'} allowed`, () => { + spec.isBidRequestValid.callsFake(br => { + checkBidRequest(br); + return true; + }); + spec.buildRequests.callsFake((bidReqs, bidderReq) => { + checkBidderRequest(bidderReq); + bidReqs.forEach(checkBidRequest); + return {method: 'POST'}; + }); + activityRules.isActivityAllowed.callsFake(() => allowed); + + const bidder = newBidder(spec); + + bidder.callBids({ + bidderCode: 'mockBidder', + auctionId: 'aid', + bids: [ + { + adUnitCode: 'mockAU', + bidId: 'bid', + transactionId: 'tid', + auctionId: 'aid' + } + ] + }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + sinon.assert.calledWithMatch(activityRules.isActivityAllowed, ACTIVITY_TRANSMIT_TID, { + componentType: MODULE_TYPE_BIDDER, + componentName: 'mockBidder' + }); + sinon.assert.calledWithMatch(addBidResponseStub, sinon.match.any, { + transactionId: 'tid', + auctionId: 'aid' + }) + }); + }); + + it('should not be hidden from request methods', (done) => { + const bidderRequest = { bidderCode: 'mockBidder', auctionId: 'aid', + getAID() { return this.auctionId }, bids: [ { adUnitCode: 'mockAU', bidId: 'bid', transactionId: 'tid', - auctionId: 'aid' + auctionId: 'aid', + getTIDs() { + return [this.auctionId, this.transactionId] + } } ] - }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - sinon.assert.calledWithMatch(activityRules.isActivityAllowed, ACTIVITY_TRANSMIT_TID, { - componentType: MODULE_TYPE_BIDDER, - componentName: 'mockBidder' + }; + activityRules.isActivityAllowed.callsFake(() => false); + spec.isBidRequestValid.returns(true); + spec.buildRequests.callsFake((reqs, bidderReq) => { + expect(bidderReq.getAID()).to.eql('aid'); + expect(reqs[0].getTIDs()).to.eql(['aid', 'tid']); + done(); }); - sinon.assert.calledWithMatch(addBidResponseStub, sinon.match.any, { - transactionId: 'tid', - auctionId: 'aid' - }) - }); + newBidder(spec).callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + }) }); - }); - it('should handle bad bid requests gracefully', function () { - const bidder = newBidder(spec); - - spec.getUserSyncs.returns([]); + it('should handle bad bid requests gracefully', function () { + const bidder = newBidder(spec); - bidder.callBids({}); - bidder.callBids({ bids: 'nothing useful' }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.getUserSyncs.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.called).to.equal(false); - expect(spec.buildRequests.called).to.equal(false); - expect(spec.interpretResponse.called).to.equal(false); - }); + bidder.callBids({}); + bidder.callBids({ bids: 'nothing useful' }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should call buildRequests(bidRequest) the params are valid', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.called).to.equal(false); + expect(spec.buildRequests.called).to.equal(false); + expect(spec.interpretResponse.called).to.equal(false); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); + it('should call buildRequests(bidRequest) the params are valid', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.calledOnce).to.equal(true); - expect(spec.buildRequests.firstCall.args[0]).to.deep.equal(MOCK_BIDS_REQUEST.bids); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not call buildRequests the params are invalid', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.calledOnce).to.equal(true); + expect(spec.buildRequests.firstCall.args[0]).to.deep.equal(MOCK_BIDS_REQUEST.bids); + }); - spec.isBidRequestValid.returns(false); - spec.buildRequests.returns([]); + it('should not call buildRequests the params are invalid', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(false); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.called).to.equal(false); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should filter out invalid bids before calling buildRequests', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.called).to.equal(false); + }); - spec.isBidRequestValid.onFirstCall().returns(true); - spec.isBidRequestValid.onSecondCall().returns(false); - spec.buildRequests.returns([]); + it('should filter out invalid bids before calling buildRequests', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.onFirstCall().returns(true); + spec.isBidRequestValid.onSecondCall().returns(false); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.calledOnce).to.equal(true); - expect(spec.buildRequests.firstCall.args[0]).to.deep.equal([MOCK_BIDS_REQUEST.bids[0]]); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make no server requests if the spec doesn\'t return any', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.calledOnce).to.equal(true); + expect(spec.buildRequests.firstCall.args[0]).to.deep.equal([MOCK_BIDS_REQUEST.bids[0]]); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); + it('should make no server requests if the spec doesn\'t return any', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate POST request', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: url, - data: data + expect(ajaxStub.called).to.equal(false); }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate POST request', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: url, + data: data + }); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(url); - expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'POST', - contentType: 'text/plain', - withCredentials: true - }); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate POST request when options are passed', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - const options = { contentType: 'application/json' }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: url, - data: data, - options: options + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(url); + expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'POST', + contentType: 'text/plain', + withCredentials: true + }); }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate POST request when options are passed', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const options = { contentType: 'application/json' }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: url, + data: data, + options: options + }); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(url); - expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'POST', - contentType: 'application/json', - withCredentials: true - }); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate GET request', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'GET', - url: url, - data: data + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(url); + expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'POST', + contentType: 'application/json', + withCredentials: true + }) }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate GET request', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'GET', + url: url, + data: data + }); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); - expect(ajaxStub.firstCall.args[2]).to.be.undefined; - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'GET', - withCredentials: true - }); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate GET request when options are passed', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - const opt = { withCredentials: false } - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'GET', - url: url, - data: data, - options: opt + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); + expect(ajaxStub.firstCall.args[2]).to.be.undefined; + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'GET', + withCredentials: true + }) }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate GET request when options are passed', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const opt = { withCredentials: false } + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'GET', + url: url, + data: data, + options: opt + }); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); - expect(ajaxStub.firstCall.args[2]).to.be.undefined; - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'GET', - withCredentials: false - }); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make multiple calls if the spec returns them', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([ - { - method: 'POST', - url: url, - data: data - }, - { + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); + expect(ajaxStub.firstCall.args[2]).to.be.undefined; + sinon.assert.match(ajaxStub.firstCall.args[3], { method: 'GET', - url: url, - data: data - } - ]); + withCredentials: false + }) + }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make multiple calls if the spec returns them', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([ + { + method: 'POST', + url: url, + data: data + }, + { + method: 'GET', + url: url, + data: data + } + ]); - expect(ajaxStub.calledTwice).to.equal(true); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not add bids for each placement code if no requests are given', function () { - const bidder = newBidder(spec); + expect(ajaxStub.calledTwice).to.equal(true); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); - spec.interpretResponse.returns([]); - spec.getUserSyncs.returns([]); + describe('browsingTopics ajax option', () => { + let transmitUfpdAllowed, bidder; + beforeEach(() => { + activityRules.isActivityAllowed.reset(); + activityRules.isActivityAllowed.callsFake((activity) => activity === ACTIVITY_TRANSMIT_UFPD ? transmitUfpdAllowed : true); + bidder = newBidder(spec); + spec.isBidRequestValid.returns(true); + }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it(`should be set to false when adapter sets browsingTopics = false`, () => { + transmitUfpdAllowed = true; + spec.buildRequests.returns([ + { + method: 'GET', + url: 'url', + options: { + browsingTopics: false + } + } + ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.calledWith(ajaxStub, 'url', sinon.match.any, sinon.match.any, sinon.match({ + browsingTopics: false + })); + }); - expect(addBidResponseStub.callCount).to.equal(0); - }); + Object.entries({ + 'allowed': true, + 'not allowed': false + }).forEach(([t, allow]) => { + it(`should be set to ${allow} when transmitUfpd is ${t}`, () => { + transmitUfpdAllowed = allow; + spec.buildRequests.returns([ + { + method: 'GET', + url: '1', + }, + { + method: 'POST', + url: '2', + data: {} + }, + { + method: 'GET', + url: '3', + options: { + browsingTopics: true + } + }, + { + method: 'POST', + url: '4', + data: {}, + options: { + browsingTopics: true + } + } + ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + ['1', '2', '3', '4'].forEach(url => { + sinon.assert.calledWith( + ajaxStub, + url, + sinon.match.any, + sinon.match.any, + sinon.match({browsingTopics: allow}) + ); + }); + }); + }); + }); - it('should emit BEFORE_BIDDER_HTTP events before network requests', function () { - const bidder = newBidder(spec); - const req = { - method: 'POST', - url: 'test.url.com', - data: { arg: 2 } - }; + it('should not add bids for each placement code if no requests are given', function () { + const bidder = newBidder(spec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([req, req]); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); + spec.interpretResponse.returns([]); + spec.getUserSyncs.returns([]); - const eventEmitterSpy = sinon.spy(events, 'emit'); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(ajaxStub.calledTwice).to.equal(true); - expect(eventEmitterSpy.getCalls() - .filter(call => call.args[0] === CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP) - ).to.length(2); + expect(addBidResponseStub.callCount).to.equal(0); + }); - eventEmitterSpy.restore(); - }); - }); + it('should emit BEFORE_BIDDER_HTTP events before network requests', function () { + const bidder = newBidder(spec); + const req = { + method: 'POST', + url: 'test.url.com', + data: { arg: 2 } + }; - describe('when the ajax call succeeds', function () { - let ajaxStub; - let userSyncStub; - let logErrorSpy; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([req, req]); - beforeEach(function () { - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - const fakeResponse = sinon.stub(); - fakeResponse.returns('headerContent'); - callbacks.success('response body', { getResponseHeader: fakeResponse }); - }); - addBidResponseStub.reset(); - doneStub.resetBehavior(); - userSyncStub = sinon.stub(userSync, 'registerSync') - logErrorSpy = sinon.spy(utils, 'logError'); - }); + const eventEmitterSpy = sinon.spy(events, 'emit'); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - afterEach(function () { - ajaxStub.restore(); - userSyncStub.restore(); - utils.logError.restore(); + expect(ajaxStub.calledTwice).to.equal(true); + expect(eventEmitterSpy.getCalls() + .filter(call => call.args[0] === CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP) + ).to.length(2); + + eventEmitterSpy.restore(); + }); }); - it('should call spec.interpretResponse() with the response content', function () { - const bidder = newBidder(spec); + describe('when the ajax call succeeds', function () { + let ajaxStub; + let userSyncStub; + let logErrorSpy; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + beforeEach(function () { + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + const fakeResponse = sinon.stub(); + fakeResponse.returns('headerContent'); + callbacks.success('response body', { getResponseHeader: fakeResponse }); + }); + addBidResponseStub.reset(); + doneStub.resetBehavior(); + userSyncStub = sinon.stub(userSync, 'registerSync') + logErrorSpy = sinon.spy(utils, 'logError'); }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(spec.interpretResponse.calledOnce).to.equal(true); - const response = spec.interpretResponse.firstCall.args[0] - expect(response.body).to.equal('response body') - expect(response.headers.get('some-header')).to.equal('headerContent'); - expect(spec.interpretResponse.firstCall.args[1]).to.deep.equal({ - method: 'POST', - url: 'test.url.com', - data: {} + afterEach(function () { + ajaxStub.restore(); + userSyncStub.restore(); + utils.logError.restore(); }); - expect(doneStub.calledOnce).to.equal(true); - }); - it('should call spec.interpretResponse() once for each request made', function () { - const bidder = newBidder(spec); + it('should call spec.interpretResponse() with the response content', function () { + const bidder = newBidder(spec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([ - { + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ method: 'POST', url: 'test.url.com', data: {} - }, - { + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.interpretResponse.calledOnce).to.equal(true); + const response = spec.interpretResponse.firstCall.args[0] + expect(response.body).to.equal('response body') + expect(response.headers.get('some-header')).to.equal('headerContent'); + expect(spec.interpretResponse.firstCall.args[1]).to.deep.equal({ method: 'POST', url: 'test.url.com', data: {} - }, - ]); - spec.getUserSyncs.returns([]); + }); + expect(doneStub.calledOnce).to.equal(true); + }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should call spec.interpretResponse() once for each request made', function () { + const bidder = newBidder(spec); - expect(spec.interpretResponse.calledTwice).to.equal(true); - expect(doneStub.calledOnce).to.equal(true); - }); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([ + { + method: 'POST', + url: 'test.url.com', + data: {} + }, + { + method: 'POST', + url: 'test.url.com', + data: {} + }, + ]); + spec.getUserSyncs.returns([]); - it('should only add bids for valid adUnit code into the auction, even if the bidder doesn\'t bid on all of them', function () { - const bidder = newBidder(spec); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - const bid = { - creativeId: 'creative-id', - requestId: '1', - ad: 'ad-url.com', - cpm: 0.5, - height: 200, - width: 300, - adUnitCode: 'mock/placement', - currency: 'USD', - netRevenue: true, - ttl: 300, - bidderCode: 'sampleBidder', - sampleBidder: {advertiserId: '12345', networkId: '111222'} - }; - const bidderRequest = Object.assign({}, MOCK_BIDS_REQUEST); - bidderRequest.bids[0].bidder = 'sampleBidder'; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + expect(spec.interpretResponse.calledTwice).to.equal(true); + expect(doneStub.calledOnce).to.equal(true); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); + it('should only add bids for valid adUnit code into the auction, even if the bidder doesn\'t bid on all of them', function () { + const bidder = newBidder(spec); - bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + const bid = { + creativeId: 'creative-id', + requestId: '1', + ad: 'ad-url.com', + cpm: 0.5, + height: 200, + width: 300, + adUnitCode: 'mock/placement', + currency: 'USD', + netRevenue: true, + ttl: 300, + bidderCode: 'sampleBidder', + sampleBidder: {advertiserId: '12345', networkId: '111222'} + }; + const bidderRequest = Object.assign({}, MOCK_BIDS_REQUEST); + bidderRequest.bids[0].bidder = 'sampleBidder'; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - let bidObject = addBidResponseStub.firstCall.args[1]; - // checking the fields added by our code - expect(bidObject.originalCpm).to.equal(bid.cpm); - expect(bidObject.originalCurrency).to.equal(bid.currency); - expect(doneStub.calledOnce).to.equal(true); - expect(logErrorSpy.callCount).to.equal(0); - expect(bidObject.meta).to.exist; - expect(bidObject.meta).to.deep.equal({advertiserId: '12345', networkId: '111222'}); - }); + spec.interpretResponse.returns(bid); - it('should call spec.getUserSyncs() with the response', function () { - const bidder = newBidder(spec); + bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + let bidObject = addBidResponseStub.firstCall.args[1]; + // checking the fields added by our code + expect(bidObject.originalCpm).to.equal(bid.cpm); + expect(bidObject.originalCurrency).to.equal(bid.currency); + expect(doneStub.calledOnce).to.equal(true); + expect(logErrorSpy.callCount).to.equal(0); + expect(bidObject.meta).to.exist; + expect(bidObject.meta).to.deep.equal({advertiserId: '12345', networkId: '111222'}); }); - spec.getUserSyncs.returns([]); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should call spec.getUserSyncs() with the response', function () { + const bidder = newBidder(spec); - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1].length).to.equal(1); - expect(spec.getUserSyncs.firstCall.args[1][0].body).to.equal('response body'); - expect(spec.getUserSyncs.firstCall.args[1][0].headers).to.have.property('get'); - expect(spec.getUserSyncs.firstCall.args[1][0].headers.get).to.be.a('function'); - }); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); - it('should register usersync pixels', function () { - const bidder = newBidder(spec); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - spec.isBidRequestValid.returns(false); - spec.buildRequests.returns([]); - spec.getUserSyncs.returns([{ - type: 'iframe', - url: 'usersync.com' - }]); + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1].length).to.equal(1); + expect(spec.getUserSyncs.firstCall.args[1][0].body).to.equal('response body'); + expect(spec.getUserSyncs.firstCall.args[1][0].headers).to.have.property('get'); + expect(spec.getUserSyncs.firstCall.args[1][0].headers.get).to.be.a('function'); + }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should register usersync pixels', function () { + const bidder = newBidder(spec); - expect(userSyncStub.called).to.equal(true); - expect(userSyncStub.firstCall.args[0]).to.equal('iframe'); - expect(userSyncStub.firstCall.args[1]).to.equal(spec.code); - expect(userSyncStub.firstCall.args[2]).to.equal('usersync.com'); - }); + spec.isBidRequestValid.returns(false); + spec.buildRequests.returns([]); + spec.getUserSyncs.returns([{ + type: 'iframe', + url: 'usersync.com' + }]); - it('should logError and reject bid when required bid response params are missing', function () { - const bidder = newBidder(spec); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - const bid = { - requestId: '1', - ad: 'ad-url.com', - cpm: 0.5, - height: 200, - width: 300, - placementCode: 'mock/placement' - }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + expect(userSyncStub.called).to.equal(true); + expect(userSyncStub.firstCall.args[0]).to.equal('iframe'); + expect(userSyncStub.firstCall.args[1]).to.equal(spec.code); + expect(userSyncStub.firstCall.args[2]).to.equal('usersync.com'); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); + it('should logError and reject bid when required bid response params are missing', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + const bid = { + requestId: '1', + ad: 'ad-url.com', + cpm: 0.5, + height: 200, + width: 300, + placementCode: 'mock/placement' + }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); - expect(logErrorSpy.calledOnce).to.equal(true); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + spec.interpretResponse.returns(bid); - it('should logError and reject bid when required response params are undefined', function () { - const bidder = newBidder(spec); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - const bid = { - 'ad': 'creative', - 'cpm': '1.99', - 'width': 300, - 'height': 250, - 'requestId': '1', - 'creativeId': 'some-id', - 'currency': undefined, - 'netRevenue': true, - 'ttl': 360 - }; + expect(logErrorSpy.calledOnce).to.equal(true); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + it('should logError and reject bid when required response params are undefined', function () { + const bidder = newBidder(spec); + + const bid = { + 'ad': 'creative', + 'cpm': '1.99', + 'width': 300, + 'height': 250, + 'requestId': '1', + 'creativeId': 'some-id', + 'currency': undefined, + 'netRevenue': true, + 'ttl': 360 + }; + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + spec.interpretResponse.returns(bid); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(logErrorSpy.calledOnce).to.equal(true); + expect(addBidResponseStub.reject.calledOnce).to.be.true; }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); + it('should require requestId from interpretResponse', () => { + const bidder = newBidder(spec); + const bid = { + 'ad': 'creative', + 'cpm': '1.99', + 'creativeId': 'some-id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + spec.interpretResponse.returns(bid); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(logErrorSpy.calledOnce).to.equal(true); - expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(addBidResponseStub.called).to.be.false; + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); }); - it('should require requestId from interpretResponse', () => { - const bidder = newBidder(spec); - const bid = { - 'ad': 'creative', - 'cpm': '1.99', - 'creativeId': 'some-id', - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 360 + describe('when the ajax call fails', function () { + let ajaxStub; + let callBidderErrorStub; + let eventEmitterStub; + let xhrErrorMock = { + status: 500, + statusText: 'Internal Server Error' }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + + beforeEach(function () { + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + callbacks.error('ajax call failed.', xhrErrorMock); + }); + callBidderErrorStub = sinon.stub(adapterManager, 'callBidderError'); + eventEmitterStub = sinon.stub(events, 'emit'); + addBidResponseStub.reset(); + doneStub.reset(); + }); + + afterEach(function () { + ajaxStub.restore(); + callBidderErrorStub.restore(); + eventEmitterStub.restore(); + }); + + it('should not spec.interpretResponse()', function () { + const bidder = newBidder(spec); + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.interpretResponse.called).to.equal(false); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); + }); + + it('should not add bids for each adunit code into the auction', function () { + const bidder = newBidder(spec); + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.interpretResponse.returns([]); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.callCount).to.equal(0); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); + }); + + it('should call spec.getUserSyncs() with no responses', function () { + const bidder = newBidder(spec); + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should call spec.getUserSyncs() with no responses', function () { + const bidder = newBidder(spec); - expect(addBidResponseStub.called).to.be.false; - expect(addBidResponseStub.reject.calledOnce).to.be.true; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); + }); }); }); - describe('when the ajax call fails', function () { - let ajaxStub; - let callBidderErrorStub; - let eventEmitterStub; - let xhrErrorMock = { - status: 500, - statusText: 'Internal Server Error' - }; + describe('registerBidder', function () { + let registerBidAdapterStub; + let aliasBidAdapterStub; beforeEach(function () { - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - callbacks.error('ajax call failed.', xhrErrorMock); - }); - callBidderErrorStub = sinon.stub(adapterManager, 'callBidderError'); - eventEmitterStub = sinon.stub(events, 'emit'); - addBidResponseStub.reset(); - doneStub.reset(); + registerBidAdapterStub = sinon.stub(adapterManager, 'registerBidAdapter'); + aliasBidAdapterStub = sinon.stub(adapterManager, 'aliasBidAdapter'); }); afterEach(function () { - ajaxStub.restore(); - callBidderErrorStub.restore(); - eventEmitterStub.restore(); + registerBidAdapterStub.restore(); + aliasBidAdapterStub.restore(); }); - it('should not spec.interpretResponse()', function () { - const bidder = newBidder(spec); + function newEmptySpec() { + return { + code: CODE, + isBidRequestValid: function() { }, + buildRequests: function() { }, + interpretResponse: function() { }, + }; + } - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.interpretResponse.called).to.equal(false); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); - }); + it('should register a bidder with the adapterManager', function () { + registerBidder(newEmptySpec()); + expect(registerBidAdapterStub.calledOnce).to.equal(true); + expect(registerBidAdapterStub.firstCall.args[0]).to.have.property('callBids'); + expect(registerBidAdapterStub.firstCall.args[0].callBids).to.be.a('function'); - it('should not add bids for each adunit code into the auction', function () { - const bidder = newBidder(spec); + expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); + expect(registerBidAdapterStub.firstCall.args[2]).to.be.undefined; + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.interpretResponse.returns([]); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(addBidResponseStub.callCount).to.equal(0); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); + it('should register a bidder with the appropriate mediaTypes', function () { + const thisSpec = Object.assign(newEmptySpec(), { supportedMediaTypes: ['video'] }); + registerBidder(thisSpec); + expect(registerBidAdapterStub.calledOnce).to.equal(true); + expect(registerBidAdapterStub.firstCall.args[2]).to.deep.equal({supportedMediaTypes: ['video']}); }); - it('should call spec.getUserSyncs() with no responses', function () { - const bidder = newBidder(spec); + it('should register bidders with the appropriate aliases', function () { + const thisSpec = Object.assign(newEmptySpec(), { aliases: ['foo', 'bar'] }); + registerBidder(thisSpec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); + expect(registerBidAdapterStub.calledThrice).to.equal(true); + + // Make sure our later calls don't override the bidder code from previous calls. + expect(registerBidAdapterStub.firstCall.args[0].getBidderCode()).to.equal(CODE); + expect(registerBidAdapterStub.secondCall.args[0].getBidderCode()).to.equal('foo') + expect(registerBidAdapterStub.thirdCall.args[0].getBidderCode()).to.equal('bar') + + expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); + expect(registerBidAdapterStub.secondCall.args[1]).to.equal('foo') + expect(registerBidAdapterStub.thirdCall.args[1]).to.equal('bar') }); - it('should call spec.getUserSyncs() with no responses', function () { - const bidder = newBidder(spec); + it('should register alias with their gvlid', function() { + const aliases = [ + { + code: 'foo', + gvlid: 1 + }, + { + code: 'bar', + gvlid: 2 + }, + { + code: 'baz' + } + ] + const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); + registerBidder(thisSpec); + + expect(registerBidAdapterStub.getCall(1).args[0].getSpec().gvlid).to.equal(1); + expect(registerBidAdapterStub.getCall(2).args[0].getSpec().gvlid).to.equal(2); + expect(registerBidAdapterStub.getCall(3).args[0].getSpec().gvlid).to.equal(undefined); + }) + + it('should register alias with skipPbsAliasing', function() { + const aliases = [ + { + code: 'foo', + skipPbsAliasing: true + }, + { + code: 'bar', + skipPbsAliasing: false + }, + { + code: 'baz' + } + ] + const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); + registerBidder(thisSpec); + + expect(registerBidAdapterStub.getCall(1).args[0].getSpec().skipPbsAliasing).to.equal(true); + expect(registerBidAdapterStub.getCall(2).args[0].getSpec().skipPbsAliasing).to.equal(false); + expect(registerBidAdapterStub.getCall(3).args[0].getSpec().skipPbsAliasing).to.equal(undefined); + }) + }) + + describe('validate bid response: ', function () { + let spec; + let indexStub, adUnits, bidderRequests; + let addBidResponseStub; + let doneStub; + let ajaxStub; + let logErrorSpy; + + let bids = [{ + 'ad': 'creative', + 'cpm': '1.99', + 'width': 300, + 'height': 250, + 'requestId': '1', + 'creativeId': 'some-id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }]; + + beforeEach(function () { + spec = { + code: CODE, + isBidRequestValid: sinon.stub(), + buildRequests: sinon.stub(), + interpretResponse: sinon.stub(), + }; spec.isBidRequestValid.returns(true); spec.buildRequests.returns({ @@ -776,212 +1031,138 @@ describe('bidders created by newBidder', function () { url: 'test.url.com', data: {} }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST + + addBidResponseStub = sinon.stub(); + addBidResponseStub.reject = sinon.stub(); + doneStub = sinon.stub(); + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + const fakeResponse = sinon.stub(); + fakeResponse.returns('headerContent'); + callbacks.success('response body', { getResponseHeader: fakeResponse }); }); + logErrorSpy = sinon.spy(utils, 'logError'); + indexStub = sinon.stub(auctionManager, 'index'); + adUnits = []; + bidderRequests = []; + indexStub.get(() => stubAuctionIndex({adUnits: adUnits, bidderRequests: bidderRequests})) }); - }); -}); -describe('registerBidder', function () { - let registerBidAdapterStub; - let aliasBidAdapterStub; - - beforeEach(function () { - registerBidAdapterStub = sinon.stub(adapterManager, 'registerBidAdapter'); - aliasBidAdapterStub = sinon.stub(adapterManager, 'aliasBidAdapter'); - }); - - afterEach(function () { - registerBidAdapterStub.restore(); - aliasBidAdapterStub.restore(); - }); - - function newEmptySpec() { - return { - code: CODE, - isBidRequestValid: function() { }, - buildRequests: function() { }, - interpretResponse: function() { }, - }; - } - - it('should register a bidder with the adapterManager', function () { - registerBidder(newEmptySpec()); - expect(registerBidAdapterStub.calledOnce).to.equal(true); - expect(registerBidAdapterStub.firstCall.args[0]).to.have.property('callBids'); - expect(registerBidAdapterStub.firstCall.args[0].callBids).to.be.a('function'); - - expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); - expect(registerBidAdapterStub.firstCall.args[2]).to.be.undefined; - }); - - it('should register a bidder with the appropriate mediaTypes', function () { - const thisSpec = Object.assign(newEmptySpec(), { supportedMediaTypes: ['video'] }); - registerBidder(thisSpec); - expect(registerBidAdapterStub.calledOnce).to.equal(true); - expect(registerBidAdapterStub.firstCall.args[2]).to.deep.equal({supportedMediaTypes: ['video']}); - }); - - it('should register bidders with the appropriate aliases', function () { - const thisSpec = Object.assign(newEmptySpec(), { aliases: ['foo', 'bar'] }); - registerBidder(thisSpec); - - expect(registerBidAdapterStub.calledThrice).to.equal(true); - - // Make sure our later calls don't override the bidder code from previous calls. - expect(registerBidAdapterStub.firstCall.args[0].getBidderCode()).to.equal(CODE); - expect(registerBidAdapterStub.secondCall.args[0].getBidderCode()).to.equal('foo') - expect(registerBidAdapterStub.thirdCall.args[0].getBidderCode()).to.equal('bar') - - expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); - expect(registerBidAdapterStub.secondCall.args[1]).to.equal('foo') - expect(registerBidAdapterStub.thirdCall.args[1]).to.equal('bar') - }); + afterEach(function () { + ajaxStub.restore(); + logErrorSpy.restore(); + indexStub.restore; + }); - it('should register alias with their gvlid', function() { - const aliases = [ - { - code: 'foo', - gvlid: 1 - }, - { - code: 'bar', - gvlid: 2 - }, - { - code: 'baz' - } - ] - const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); - registerBidder(thisSpec); + if (FEATURES.NATIVE) { + it('should add native bids that do have required assets', function () { + adUnits = [{ + transactionId: 'au', + nativeParams: { + title: {'required': true}, + } + }] + decorateAdUnitsWithNativeParams(adUnits); + let bidRequest = { + bids: [{ + bidId: '1', + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + transactionId: 'au', + params: { + param: 5 + }, + mediaType: 'native', + }] + }; + + let bids1 = Object.assign({}, + bids[0], + { + 'mediaType': 'native', + 'native': { + 'title': 'Native Creative', + 'clickUrl': 'https://www.link.example', + } + } + ); - expect(registerBidAdapterStub.getCall(1).args[0].getSpec().gvlid).to.equal(1); - expect(registerBidAdapterStub.getCall(2).args[0].getSpec().gvlid).to.equal(2); - expect(registerBidAdapterStub.getCall(3).args[0].getSpec().gvlid).to.equal(undefined); - }) + const bidder = newBidder(spec); - it('should register alias with skipPbsAliasing', function() { - const aliases = [ - { - code: 'foo', - skipPbsAliasing: true - }, - { - code: 'bar', - skipPbsAliasing: false - }, - { - code: 'baz' - } - ] - const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); - registerBidder(thisSpec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(registerBidAdapterStub.getCall(1).args[0].getSpec().skipPbsAliasing).to.equal(true); - expect(registerBidAdapterStub.getCall(2).args[0].getSpec().skipPbsAliasing).to.equal(false); - expect(registerBidAdapterStub.getCall(3).args[0].getSpec().skipPbsAliasing).to.equal(undefined); - }) -}) + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + expect(logErrorSpy.callCount).to.equal(0); + }); -describe('validate bid response: ', function () { - let spec; - let indexStub, adUnits, bidderRequests; - let addBidResponseStub; - let doneStub; - let ajaxStub; - let logErrorSpy; - - let bids = [{ - 'ad': 'creative', - 'cpm': '1.99', - 'width': 300, - 'height': 250, - 'requestId': '1', - 'creativeId': 'some-id', - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 360 - }]; - - beforeEach(function () { - spec = { - code: CODE, - isBidRequestValid: sinon.stub(), - buildRequests: sinon.stub(), - interpretResponse: sinon.stub(), - }; - - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); + it('should not add native bids that do not have required assets', function () { + adUnits = [{ + transactionId: 'au', + nativeParams: { + title: {'required': true}, + }, + }]; + decorateAdUnitsWithNativeParams(adUnits); + let bidRequest = { + bids: [{ + bidId: '1', + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + transactionId: 'au', + params: { + param: 5 + }, + mediaType: 'native', + }] + }; + let bids1 = Object.assign({}, + bids[0], + { + bidderCode: CODE, + mediaType: 'native', + native: { + title: undefined, + clickUrl: 'https://www.link.example', + } + } + ); - addBidResponseStub = sinon.stub(); - addBidResponseStub.reject = sinon.stub(); - doneStub = sinon.stub(); - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - const fakeResponse = sinon.stub(); - fakeResponse.returns('headerContent'); - callbacks.success('response body', { getResponseHeader: fakeResponse }); - }); - logErrorSpy = sinon.spy(utils, 'logError'); - indexStub = sinon.stub(auctionManager, 'index'); - adUnits = []; - bidderRequests = []; - indexStub.get(() => stubAuctionIndex({adUnits: adUnits, bidderRequests: bidderRequests})) - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - afterEach(function () { - ajaxStub.restore(); - logErrorSpy.restore(); - indexStub.restore; - }); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logErrorSpy.calledWithMatch('Ignoring bid: Native bid missing some required properties.')).to.equal(true); + }); + } - if (FEATURES.NATIVE) { - it('should add native bids that do have required assets', function () { + it('should add bid when renderer is present on outstream bids', function () { adUnits = [{ transactionId: 'au', - nativeParams: { - title: {'required': true}, + mediaTypes: { + video: {context: 'outstream'} } }] - decorateAdUnitsWithNativeParams(adUnits); let bidRequest = { bids: [{ bidId: '1', auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', transactionId: 'au', + adUnitCode: 'mock/placement', params: { param: 5 }, - mediaType: 'native', }] }; let bids1 = Object.assign({}, bids[0], { - 'mediaType': 'native', - 'native': { - 'title': 'Native Creative', - 'clickUrl': 'https://www.link.example', - } + bidderCode: CODE, + mediaType: 'video', + renderer: {render: () => true, url: 'render.js'}, } ); @@ -995,394 +1176,335 @@ describe('validate bid response: ', function () { expect(logErrorSpy.callCount).to.equal(0); }); - it('should not add native bids that do not have required assets', function () { - adUnits = [{ - transactionId: 'au', - nativeParams: { - title: {'required': true}, - }, - }]; - decorateAdUnitsWithNativeParams(adUnits); + it('should add banner bids that have no width or height but single adunit size', function () { let bidRequest = { bids: [{ + bidder: CODE, bidId: '1', auctionId: 'first-bid-id', adUnitCode: 'mock/placement', - transactionId: 'au', params: { param: 5 }, - mediaType: 'native', + sizes: [[300, 250]], }] }; + bidderRequests = [bidRequest]; let bids1 = Object.assign({}, bids[0], { - bidderCode: CODE, - mediaType: 'native', - native: { - title: undefined, - clickUrl: 'https://www.link.example', - } + width: undefined, + height: undefined } ); const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logErrorSpy.calledWithMatch('Ignoring bid: Native bid missing some required properties.')).to.equal(true); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + expect(logErrorSpy.callCount).to.equal(0); }); - } - - it('should add bid when renderer is present on outstream bids', function () { - adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }] - let bidRequest = { - bids: [{ - bidId: '1', - auctionId: 'first-bid-id', - transactionId: 'au', - adUnitCode: 'mock/placement', - params: { - param: 5 - }, - }] - }; - - let bids1 = Object.assign({}, - bids[0], - { - bidderCode: CODE, - mediaType: 'video', - renderer: {render: () => true, url: 'render.js'}, - } - ); - - const bidder = newBidder(spec); - - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - expect(logErrorSpy.callCount).to.equal(0); - }); - - it('should add banner bids that have no width or height but single adunit size', function () { - let bidRequest = { - bids: [{ - bidder: CODE, - bidId: '1', - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - params: { - param: 5 - }, - sizes: [[300, 250]], - }] - }; - bidderRequests = [bidRequest]; - let bids1 = Object.assign({}, - bids[0], - { - width: undefined, - height: undefined - } - ); - - const bidder = newBidder(spec); - - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - expect(logErrorSpy.callCount).to.equal(0); - }); - - describe(' Check for alternateBiddersList ', function() { - let bidRequest; - let bids1; - let logWarnSpy; - let bidderSettingStub, aliasRegistryStub; - let aliasRegistry; - beforeEach(function () { - bidRequest = { + it('should disregard auctionId/transactionId set by the adapter', () => { + let bidderRequest = { bids: [{ - bidId: '1', bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - transactionId: 'au', + bidId: '1', + auctionId: 'aid', + transactionId: 'tid', + adUnitCode: 'au', }] }; + const bidder = newBidder(spec); + spec.interpretResponse.returns(Object.assign({}, bids[0], {transactionId: 'ignored', auctionId: 'ignored'})); + bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.calledWith(addBidResponseStub, sinon.match.any, sinon.match({ + transactionId: 'tid', + auctionId: 'aid' + })); + }) - bids1 = Object.assign({}, - bids[0], - { - bidderCode: 'validalternatebidder', - adapterCode: 'knownadapter1' - } - ); - logWarnSpy = sinon.spy(utils, 'logWarn'); - bidderSettingStub = sinon.stub(bidderSettings, 'get'); - aliasRegistry = {}; - aliasRegistryStub = sinon.stub(adapterManager, 'aliasRegistry'); - aliasRegistryStub.get(() => aliasRegistry); - }); + describe(' Check for alternateBiddersList ', function() { + let bidRequest; + let bids1; + let logWarnSpy; + let bidderSettingStub, aliasRegistryStub; + let aliasRegistry; - afterEach(function () { - logWarnSpy.restore(); - bidderSettingStub.restore(); - aliasRegistryStub.restore(); - }); + beforeEach(function () { + bidRequest = { + bids: [{ + bidId: '1', + bidder: CODE, + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + transactionId: 'au', + }] + }; + + bids1 = Object.assign({}, + bids[0], + { + bidderCode: 'validalternatebidder', + adapterCode: 'knownadapter1' + } + ); + logWarnSpy = sinon.spy(utils, 'logWarn'); + bidderSettingStub = sinon.stub(bidderSettings, 'get'); + aliasRegistry = {}; + aliasRegistryStub = sinon.stub(adapterManager, 'aliasRegistry'); + aliasRegistryStub.get(() => aliasRegistry); + }); - it('should log warning when bidder is unknown and allowAlternateBidderCodes flag is false', function () { - bidderSettingStub.returns(false); + afterEach(function () { + logWarnSpy.restore(); + bidderSettingStub.restore(); + aliasRegistryStub.restore(); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should log warning when bidder is unknown and allowAlternateBidderCodes flag is false', function () { + bidderSettingStub.returns(false); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should reject the bid, when allowAlternateBidderCodes flag is undefined (default should be false)', function () { - bidderSettingStub.returns(undefined); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should reject the bid, when allowAlternateBidderCodes flag is undefined (default should be false)', function () { + bidderSettingStub.returns(undefined); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should log warning when the particular bidder is not specified in allowedAlternateBidderCodes and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['invalidAlternateBidder02']); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should log warning when the particular bidder is not specified in allowedAlternateBidderCodes and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['invalidAlternateBidder02']); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes is empty and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes is empty and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes is marked as * and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['*']); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes is marked as * and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['*']); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes is marked as * (with space) and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([' * ']); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes is marked as * (with space) and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([' * ']); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not accept the bid, when allowedAlternateBidderCodes is marked as empty array and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([]); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not accept the bid, when allowedAlternateBidderCodes is marked as empty array and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([]); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes contains bidder name and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['validAlternateBidder']); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes contains bidder name and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['validAlternateBidder']); - expect(addBidResponseStub.called).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not accept the bid, when bidder is an alias but bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); - aliasRegistry = {'validAlternateBidder': CODE}; + expect(addBidResponseStub.called).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not accept the bid, when bidder is an alias but bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); + aliasRegistry = {'validAlternateBidder': CODE}; - expect(addBidResponseStub.called).to.equal(false); - expect(logWarnSpy.callCount).to.equal(1); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not accept the bid, when bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); + expect(addBidResponseStub.called).to.equal(false); + expect(logWarnSpy.callCount).to.equal(1); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not accept the bid, when bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); }); - }); - describe('when interpretResponse returns BidderAuctionResponse', function() { - const bidRequest = { - bids: [{ + describe('when interpretResponse returns BidderAuctionResponse', function() { + const bidRequest = { + auctionId: 'aid', + bids: [{ + bidId: '1', + bidder: CODE, + auctionId: 'aid', + adUnitCode: 'mock/placement', + transactionId: 'au', + }] + }; + const fledgeAuctionConfig = { bidId: '1', - bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - transactionId: 'au', - }] - }; - const fledgeAuctionConfig = { - bidId: '1', - config: { - foo: 'bar' - } - } - describe('when response has FLEDGE auction config', function() { - let fledgeStub; - - function fledgeHook(next, ...args) { - fledgeStub(...args); + config: { + foo: 'bar' + } } + describe('when response has FLEDGE auction config', function() { + let fledgeStub; - before(() => { - addComponentAuction.before(fledgeHook); - }); + function fledgeHook(next, ...args) { + fledgeStub(...args); + } - after(() => { - addComponentAuction.getHooks({hook: fledgeHook}).remove(); - }) + before(() => { + addComponentAuction.before(fledgeHook); + }); - beforeEach(function () { - fledgeStub = sinon.stub(); - }); + after(() => { + addComponentAuction.getHooks({hook: fledgeHook}).remove(); + }) - it('should unwrap bids', function() { - const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: bids, - fledgeAuctionConfigs: [] + beforeEach(function () { + fledgeStub = sinon.stub(); }); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - }); - it('should call fledgeManager with FLEDGE configs', function() { - const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: bids, - fledgeAuctionConfigs: [fledgeAuctionConfig] + it('should unwrap bids', function() { + const bidder = newBidder(spec); + spec.interpretResponse.returns({ + bids: bids, + fledgeAuctionConfigs: [] + }); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); }); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(fledgeStub.calledOnce).to.equal(true); - sinon.assert.calledWith(fledgeStub, 'mock/placement', fledgeAuctionConfig.config); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - }) + it('should call fledgeManager with FLEDGE configs', function() { + const bidder = newBidder(spec); + spec.interpretResponse.returns({ + bids: bids, + fledgeAuctionConfigs: [fledgeAuctionConfig] + }); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should call fledgeManager with FLEDGE configs even if no bids returned', function() { - const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: [], - fledgeAuctionConfigs: [fledgeAuctionConfig] - }); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(fledgeStub.calledOnce).to.equal(true); + sinon.assert.calledWith(fledgeStub, bidRequest.bids[0], fledgeAuctionConfig.config); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + }) + + it('should call fledgeManager with FLEDGE configs even if no bids returned', function() { + const bidder = newBidder(spec); + spec.interpretResponse.returns({ + bids: [], + fledgeAuctionConfigs: [fledgeAuctionConfig] + }); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(fledgeStub.calledOnce).to.be.true; - sinon.assert.calledWith(fledgeStub, 'mock/placement', fledgeAuctionConfig.config); - expect(addBidResponseStub.calledOnce).to.equal(false); + expect(fledgeStub.calledOnce).to.be.true; + sinon.assert.calledWith(fledgeStub, bidRequest.bids[0], fledgeAuctionConfig.config); + expect(addBidResponseStub.calledOnce).to.equal(false); + }) }) }) - }) -}); + }); -describe('bid response isValid', () => { - describe('size check', () => { - let req, index; + describe('bid response isValid', () => { + describe('size check', () => { + let req, index; - beforeEach(() => { - req = { - ...MOCK_BIDS_REQUEST.bids[0], - mediaTypes: { - banner: { - sizes: [[1, 2], [3, 4]] + beforeEach(() => { + req = { + ...MOCK_BIDS_REQUEST.bids[0], + mediaTypes: { + banner: { + sizes: [[1, 2], [3, 4]] + } } } - } - }); + }); - function mkResponse(width, height) { - return { - requestId: req.bidId, - width, - height, - cpm: 1, - ttl: 60, - creativeId: '123', - netRevenue: true, - currency: 'USD', - mediaType: 'banner', + function mkResponse(width, height) { + return { + requestId: req.bidId, + width, + height, + cpm: 1, + ttl: 60, + creativeId: '123', + netRevenue: true, + currency: 'USD', + mediaType: 'banner', + } } - } - function checkValid(bid) { - return isValid('au', bid, {index: stubAuctionIndex({bidRequests: [req]})}); - } + function checkValid(bid) { + return isValid('au', bid, {index: stubAuctionIndex({bidRequests: [req]})}); + } - it('should succeed when response has a size that was in request', () => { - expect(checkValid(mkResponse(3, 4))).to.be.true; - }); - }) -}); + it('should succeed when response has a size that was in request', () => { + expect(checkValid(mkResponse(3, 4))).to.be.true; + }); + }) + }); +}) diff --git a/test/spec/unit/core/consentHandler_spec.js b/test/spec/unit/core/consentHandler_spec.js index 98b317e0d36..1bcad3216ce 100644 --- a/test/spec/unit/core/consentHandler_spec.js +++ b/test/spec/unit/core/consentHandler_spec.js @@ -1,4 +1,4 @@ -import {ConsentHandler, gvlidRegistry} from '../../../../src/consentHandler.js'; +import {ConsentHandler, gvlidRegistry, multiHandler} from '../../../../src/consentHandler.js'; describe('Consent data handler', () => { let handler; @@ -56,6 +56,88 @@ describe('Consent data handler', () => { }) }) }); + + describe('getHash', () => { + it('is defined when null', () => { + expect(handler.hash).be.a('string'); + }); + it('changes when a field is updated', () => { + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: false}); + const h2 = handler.hash; + expect(h2).to.not.eql(h1); + handler.setConsentData({field: 'value', enabled: true}); + const h3 = handler.hash; + expect(h3).to.not.eql(h2); + expect(h3).to.not.eql(h1); + }); + it('does not change when fields are unchanged', () => { + handler.setConsentData({field: 'value', enabled: true}); + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: true}); + expect(handler.hash).to.eql(h1); + }); + it('does not change when non-hashFields are updated', () => { + handler.hashFields = ['field', 'enabled']; + handler.setConsentData({field: 'value', enabled: true}); + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: true, other: 'data'}); + expect(handler.hash).to.eql(h1); + }) + }) +}); + +describe('multiHandler', () => { + let handlers, multi; + beforeEach(() => { + handlers = {h1: {}, h2: {}}; + multi = multiHandler(handlers); + }); + + ['getConsentData', 'getConsentMeta'].forEach(method => { + describe(method, () => { + it('combines results from underlying handlers', () => { + handlers.h1[method] = () => 'one'; + handlers.h2[method] = () => 'two'; + expect(multi[method]()).to.eql({ + h1: 'one', + h2: 'two', + }) + }); + }); + }); + + describe('.promise', () => { + it('resolves all underlying promises', (done) => { + handlers.h1.promise = Promise.resolve('one'); + let resolver, result; + handlers.h2.promise = new Promise((resolve) => { resolver = resolve }); + multi.promise.then((val) => { + result = val; + expect(result).to.eql({ + h1: 'one', + h2: 'two' + }); + done(); + }) + handlers.h1.promise.then(() => { + expect(result).to.not.exist; + resolver('two'); + }); + }) + }); + + describe('.hash', () => { + ['h1', 'h2'].forEach((handler, i) => { + it(`changes when handler #${i + 1} changes hash`, () => { + handlers.h1.hash = 'one'; + handlers.h2.hash = 'two' + const first = multi.hash; + handlers[handler].hash = 'new'; + expect(multi.hash).to.not.eql(first); + }) + }) + }) }) describe('gvlidRegistry', () => { diff --git a/test/spec/unit/core/events_spec.js b/test/spec/unit/core/events_spec.js new file mode 100644 index 00000000000..6551c9f2456 --- /dev/null +++ b/test/spec/unit/core/events_spec.js @@ -0,0 +1,30 @@ +import {config} from 'src/config.js'; +import {emit, clearEvents, getEvents} from '../../../../src/events.js'; + +describe('events', () => { + let clock; + beforeEach(() => { + clock = sinon.useFakeTimers(); + clearEvents(); + }); + afterEach(() => { + clock.restore(); + }); + + it('should clear event log using eventHistoryTTL config', () => { + emit('testEvent', {}); + expect(getEvents().length).to.eql(1); + config.setConfig({eventHistoryTTL: 1}); + clock.tick(500); + expect(getEvents().length).to.eql(1); + clock.tick(6000); + expect(getEvents().length).to.eql(0); + }); + + it('should take history TTL in seconds', () => { + emit('testEvent', {}); + config.setConfig({eventHistoryTTL: 1000}); + clock.tick(10000); + expect(getEvents().length).to.eql(1); + }) +}) diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index f16d6208087..ba9aeff70d1 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -1,13 +1,19 @@ -import { expect } from 'chai'; -import { targeting as targetingInstance, filters, getHighestCpmBidsFromBidPool, sortByDealAndPriceBucketOrCpm } from 'src/targeting.js'; -import { config } from 'src/config.js'; -import { createBidReceived } from 'test/fixtures/fixtures.js'; +import {expect} from 'chai'; +import { + filters, + getHighestCpmBidsFromBidPool, + sortByDealAndPriceBucketOrCpm, + targeting as targetingInstance +} from 'src/targeting.js'; +import {config} from 'src/config.js'; +import {createBidReceived} from 'test/fixtures/fixtures.js'; import CONSTANTS from 'src/constants.json'; -import { auctionManager } from 'src/auctionManager.js'; +import {auctionManager} from 'src/auctionManager.js'; import * as utils from 'src/utils.js'; import {deepClone} from 'src/utils.js'; import {createBid} from '../../../../src/bidfactory.js'; import {hook} from '../../../../src/hook.js'; +import {getHighestCpm} from '../../../../src/utils/reducers.js'; function mkBid(bid, status = CONSTANTS.STATUS.GOOD) { return Object.assign(createBid(status), bid); @@ -451,7 +457,7 @@ describe('targeting tests', function () { } }); - const bids = getHighestCpmBidsFromBidPool(bidsReceived, utils.getHighestCpm, 2); + const bids = getHighestCpmBidsFromBidPool(bidsReceived, getHighestCpm, 2); expect(bids.length).to.equal(3); expect(bids[0].adId).to.equal('8383838'); @@ -467,7 +473,7 @@ describe('targeting tests', function () { } }); - const bids = getHighestCpmBidsFromBidPool(bidsReceived, utils.getHighestCpm, 2); + const bids = getHighestCpmBidsFromBidPool(bidsReceived, getHighestCpm, 2); expect(bids.length).to.equal(3); expect(bids[0].adId).to.equal('8383838'); @@ -949,6 +955,7 @@ describe('targeting tests', function () { expect(bids.length).to.equal(1); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); useBidCache = false; @@ -956,6 +963,7 @@ describe('targeting tests', function () { expect(bids.length).to.equal(1); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); }); it('should use bidCacheFilterFunction', function() { @@ -983,9 +991,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-5'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching Off, No Filter Function useBidCache = false; @@ -994,9 +1006,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching On AGAIN, No Filter Function (should be same as first time) useBidCache = true; @@ -1005,9 +1021,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-5'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching On, with Filter Function to Exclude video useBidCache = true; @@ -1020,9 +1040,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // filter function should have been called for each cached bid (4 times) expect(bcffCalled).to.equal(4); @@ -1038,9 +1062,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // filter function should not have been called expect(bcffCalled).to.equal(0); }); diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 5c361d186c0..664f7ebb58f 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -25,7 +25,7 @@ import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {createBid} from '../../../src/bidfactory.js'; import {enrichFPD} from '../../../src/fpd/enrichment.js'; import {mockFpdEnrichments} from '../../helpers/fpd.js'; - +import {generateUUID} from '../../../src/utils.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -43,13 +43,13 @@ var adUnits = getAdUnits(); var adUnitCodes = getAdUnits().map(unit => unit.code); var bidsBackHandler = function() {}; const timeout = 2000; -var auction = auctionManager.createAuction({adUnits, adUnitCodes, callback: bidsBackHandler, cbTimeout: timeout}); -auction.getBidRequests = getBidRequests; -auction.getBidsReceived = getBidResponses; -auction.getAdUnits = getAdUnits; -auction.getAuctionStatus = function() { return auctionModule.AUCTION_COMPLETED } +const auctionId = generateUUID(); +let auction; function resetAuction() { + if (auction == null) { + auction = auctionManager.createAuction({adUnits, adUnitCodes, callback: bidsBackHandler, cbTimeout: timeout, labels: undefined, auctionId: auctionId}); + } $$PREBID_GLOBAL$$.setConfig({ enableSendAllBids: false }); auction.getBidRequests = getBidRequests; auction.getBidsReceived = getBidResponses; @@ -3304,16 +3304,20 @@ describe('Unit: Prebid Module', function () { const highestBid = $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode('/19968336/header-bid-tag-0'); expect(highestBid).to.deep.equal(_bidsReceived[2]) }) - }) + }); - describe('getHighestCpm', () => { + describe('getHighestCpmBids', () => { after(() => { resetAuction(); }); it('returns an array containing the highest bid object for the given adUnitCode', function () { - const highestCpmBids = $$PREBID_GLOBAL$$.getHighestCpmBids('/19968336/header-bid-tag-0'); + const adUnitcode = '/19968336/header-bid-tag-0'; + targeting.setLatestAuctionForAdUnit(adUnitcode, auctionId) + const highestCpmBids = $$PREBID_GLOBAL$$.getHighestCpmBids(adUnitcode); expect(highestCpmBids.length).to.equal(1); - expect(highestCpmBids[0]).to.deep.equal(auctionManager.getBidsReceived()[1]); + const expectedBid = auctionManager.getBidsReceived()[1]; + expectedBid.latestTargetedAuctionId = auctionId; + expect(highestCpmBids[0]).to.deep.equal(expectedBid); }); it('returns an empty array when the given adUnit is not found', function () { diff --git a/test/spec/unit/utils/reducers_spec.js b/test/spec/unit/utils/reducers_spec.js new file mode 100644 index 00000000000..95bf3b74041 --- /dev/null +++ b/test/spec/unit/utils/reducers_spec.js @@ -0,0 +1,124 @@ +import { + tiebreakCompare, + keyCompare, + simpleCompare, + minimum, + maximum, + getHighestCpm, + getOldestHighestCpmBid, getLatestHighestCpmBid, reverseCompare +} from '../../../../src/utils/reducers.js'; +import assert from 'assert'; + +describe('reducers', () => { + describe('simpleCompare', () => { + Object.entries({ + '<': [10, 20, -1], + '===': [123, 123, 0], + '>': [30, -10, 1] + }).forEach(([t, [a, b, expected]]) => { + it(`returns ${expected} when a ${t} b`, () => { + expect(simpleCompare(a, b)).to.equal(expected); + }) + }) + }); + + describe('keyCompare', () => { + Object.entries({ + '<': [{k: -123}, {k: 0}, -1], + '===': [{k: 0}, {k: 0}, 0], + '>': [{k: 2}, {k: 1}, 1] + }).forEach(([t, [a, b, expected]]) => { + it(`returns ${expected} when key(a) ${t} key(b)`, () => { + expect(keyCompare(item => item.k)(a, b)).to.equal(expected); + }) + }) + }); + + describe('tiebreakCompare', () => { + Object.entries({ + 'first compare says a < b': [{main: 1, tie: 2}, {main: 2, tie: 1}, -1], + 'first compare says a > b': [{main: 2, tie: 1}, {main: 1, tie: 2}, 1], + 'first compare ties, second says a < b': [{main: 0, tie: 1}, {main: 0, tie: 2}, -1], + 'first compare ties, second says a > b': [{main: 0, tie: 2}, {main: 0, tie: 1}, 1], + 'all compares tie': [{main: 0, tie: 0}, {main: 0, tie: 0}, 0] + }).forEach(([t, [a, b, expected]]) => { + it(`should return ${expected} when ${t}`, () => { + const cmp = tiebreakCompare(keyCompare(item => item.main), keyCompare(item => item.tie)); + expect(cmp(a, b)).to.equal(expected); + }) + }) + }); + + const SAMPLE_ARR = [-10, 20, 20, 123, 400]; + + Object.entries({ + 'minimum': [minimum, ['minimum', -10], ['maximum', 400]], + 'maximum': [maximum, ['maximum', 400], ['minimum', -10]] + }).forEach(([t, [fn, simple, reversed]]) => { + describe(t, () => { + it(`should find ${simple[0]} using simple compare`, () => { + expect(SAMPLE_ARR.reduce(fn(simpleCompare))).to.equal(simple[1]); + }); + it(`should find ${reversed[0]} using reverse compare`, () => { + expect(SAMPLE_ARR.reduce(fn(reverseCompare()))).to.equal(reversed[1]); + }); + }) + }); + + describe('getHighestCpm', function () { + it('should pick the highest cpm', function () { + let a = { + cpm: 2, + timeToRespond: 100 + }; + let b = { + cpm: 1, + timeToRespond: 100 + }; + expect(getHighestCpm(a, b)).to.eql(a); + expect(getHighestCpm(b, a)).to.eql(a); + }); + + it('should pick the lowest timeToRespond cpm in case of tie', function () { + let a = { + cpm: 1, + timeToRespond: 100 + }; + let b = { + cpm: 1, + timeToRespond: 50 + }; + expect(getHighestCpm(a, b)).to.eql(b); + expect(getHighestCpm(b, a)).to.eql(b); + }); + }); + + describe('getOldestHighestCpmBid', () => { + it('should pick the oldest in case of tie using responseTimeStamp', function () { + let a = { + cpm: 1, + responseTimestamp: 1000 + }; + let b = { + cpm: 1, + responseTimestamp: 2000 + }; + expect(getOldestHighestCpmBid(a, b)).to.eql(a); + expect(getOldestHighestCpmBid(b, a)).to.eql(a); + }); + }); + describe('getLatestHighestCpmBid', () => { + it('should pick the latest in case of tie using responseTimeStamp', function () { + let a = { + cpm: 1, + responseTimestamp: 1000 + }; + let b = { + cpm: 1, + responseTimestamp: 2000 + }; + expect(getLatestHighestCpmBid(a, b)).to.eql(b); + expect(getLatestHighestCpmBid(b, a)).to.eql(b); + }); + }); +}) diff --git a/test/spec/unit/utils/ttlCollection_spec.js b/test/spec/unit/utils/ttlCollection_spec.js new file mode 100644 index 00000000000..29c6c438855 --- /dev/null +++ b/test/spec/unit/utils/ttlCollection_spec.js @@ -0,0 +1,180 @@ +import {ttlCollection} from '../../../../src/utils/ttlCollection.js'; + +describe('ttlCollection', () => { + it('can add & retrieve items', () => { + const coll = ttlCollection(); + expect(coll.toArray()).to.eql([]); + coll.add(1); + coll.add(2); + expect(coll.toArray()).to.eql([1, 2]); + }); + + it('can clear', () => { + const coll = ttlCollection(); + coll.add('item'); + coll.clear(); + expect(coll.toArray()).to.eql([]); + }); + + it('can be iterated over', () => { + const coll = ttlCollection(); + coll.add('1'); + coll.add('2'); + expect(Array.from(coll)).to.eql(['1', '2']); + }) + + describe('autopurge', () => { + let clock, pms, waitForPromises; + const SLACK = 2000; + beforeEach(() => { + clock = sinon.useFakeTimers(); + pms = []; + waitForPromises = () => Promise.all(pms); + }); + afterEach(() => { + clock.restore(); + }); + + Object.entries({ + 'defer': (value) => { + const pm = Promise.resolve(value); + pms.push(pm); + return pm; + }, + 'do not defer': (value) => value, + }).forEach(([t, resolve]) => { + describe(`when ttl/startTime ${t}`, () => { + let coll; + beforeEach(() => { + coll = ttlCollection({ + startTime: (item) => resolve(item.start == null ? new Date().getTime() : item.start), + ttl: (item) => resolve(item.ttl), + slack: SLACK + }) + }); + + it('should clear items after enough time has passed', () => { + coll.add({no: 'ttl'}); + coll.add({ttl: 1000}); + coll.add({ttl: 4000}); + return waitForPromises().then(() => { + clock.tick(500); + expect(coll.toArray()).to.eql([{no: 'ttl'}, {ttl: 1000}, {ttl: 4000}]); + clock.tick(SLACK + 500); + expect(coll.toArray()).to.eql([{no: 'ttl'}, {ttl: 4000}]); + clock.tick(3000); + expect(coll.toArray()).to.eql([{no: 'ttl'}]); + }); + }); + + it('should not wait too long if a shorter ttl shows up', () => { + coll.add({ttl: 4000}); + coll.add({ttl: 1000}); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK); + expect(coll.toArray()).to.eql([ + {ttl: 4000} + ]); + }); + }); + + it('should not wait more if later ttls are within slack', () => { + coll.add({start: 0, ttl: 4000}); + return waitForPromises().then(() => { + clock.tick(4000); + coll.add({start: 0, ttl: 5000}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + + it('should clear items ASAP if they expire in the past', () => { + clock.tick(10000); + coll.add({start: 0, ttl: 1000}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + + it('should clear items ASAP if they have ttl = 0', () => { + coll.add({ttl: 0}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + + describe('refresh', () => { + it('should refresh missing TTLs', () => { + const item = {}; + coll.add(item); + return waitForPromises().then(() => { + item.ttl = 1000; + return waitForPromises().then(() => { + clock.tick(1000 + SLACK); + expect(coll.toArray()).to.eql([item]); + coll.refresh(); + return waitForPromises().then(() => { + clock.tick(1); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + }); + + it('should refresh existing TTLs', () => { + const item = { + ttl: 1000 + }; + coll.add(item); + return waitForPromises().then(() => { + clock.tick(1000); + item.ttl = 4000; + coll.refresh(); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([item]); + clock.tick(3000); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + + it('should discard initial TTL if it does not resolve before a refresh', () => { + let resolveTTL; + const item = { + ttl: new Promise((resolve) => { + resolveTTL = resolve; + }) + }; + coll.add(item); + item.ttl = null; + coll.refresh(); + resolveTTL(1000); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK + 1000); + expect(coll.toArray()).to.eql([item]); + }); + }); + + it('should discard TTLs on clear', () => { + const item = { + ttl: 1000 + }; + coll.add(item); + coll.clear(); + item.ttl = null; + coll.add(item); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK + 1000); + expect(coll.toArray()).to.eql([item]); + }); + }); + }); + }); + }); + }); +}); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index e26683074c8..098582c0af6 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -2,7 +2,9 @@ import {getAdServerTargeting} from 'test/fixtures/fixtures.js'; import {expect} from 'chai'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; -import {deepEqual, memoize, waitForElementToLoad} from 'src/utils.js'; +import {getHighestCpm, getLatestHighestCpmBid, getOldestHighestCpmBid} from '../../src/utils/reducers.js'; +import {binarySearch, deepEqual, memoize, waitForElementToLoad} from 'src/utils.js'; +import {convertCamelToUnderscore} from '../../libraries/appnexusUtils/anUtils.js'; var assert = require('assert'); @@ -39,28 +41,6 @@ describe('Utils', function () { }); }); - describe('tryAppendQueryString', function () { - it('should append query string to existing url', function () { - var url = 'www.a.com?'; - var key = 'b'; - var value = 'c'; - - var output = utils.tryAppendQueryString(url, key, value); - - var expectedResult = url + key + '=' + encodeURIComponent(value) + '&'; - assert.equal(output, expectedResult); - }); - - it('should return existing url, if the value is empty', function () { - var url = 'www.a.com?'; - var key = 'b'; - var value = ''; - - var output = utils.tryAppendQueryString(url, key, value); - assert.equal(output, url); - }); - }); - describe('parseQueryStringParameters', function () { it('should append query string to existing using the input obj', function () { var obj = { @@ -538,72 +518,6 @@ describe('Utils', function () { }); }); - describe('getHighestCpm', function () { - it('should pick the existing highest cpm', function () { - let previous = { - cpm: 2, - timeToRespond: 100 - }; - let current = { - cpm: 1, - timeToRespond: 100 - }; - assert.equal(utils.getHighestCpm(previous, current), previous); - }); - - it('should pick the new highest cpm', function () { - let previous = { - cpm: 1, - timeToRespond: 100 - }; - let current = { - cpm: 2, - timeToRespond: 100 - }; - assert.equal(utils.getHighestCpm(previous, current), current); - }); - - it('should pick the fastest cpm in case of tie', function () { - let previous = { - cpm: 1, - timeToRespond: 100 - }; - let current = { - cpm: 1, - timeToRespond: 50 - }; - assert.equal(utils.getHighestCpm(previous, current), current); - }); - - it('should pick the oldest in case of tie using responseTimeStamp', function () { - let previous = { - cpm: 1, - timeToRespond: 100, - responseTimestamp: 1000 - }; - let current = { - cpm: 1, - timeToRespond: 50, - responseTimestamp: 2000 - }; - assert.equal(utils.getOldestHighestCpmBid(previous, current), previous); - }); - - it('should pick the latest in case of tie using responseTimeStamp', function () { - let previous = { - cpm: 1, - timeToRespond: 100, - responseTimestamp: 1000 - }; - let current = { - cpm: 1, - timeToRespond: 50, - responseTimestamp: 2000 - }; - assert.equal(utils.getLatestHighestCpmBid(previous, current), current); - }); - }); - describe('polyfill test', function () { it('should not add polyfill to array', function() { var arr = ['hello', 'world']; @@ -765,43 +679,15 @@ describe('Utils', function () { describe('convertCamelToUnderscore', function () { it('returns converted string value using underscore syntax instead of camelCase', function () { let var1 = 'placementIdTest'; - let test1 = utils.convertCamelToUnderscore(var1); + let test1 = convertCamelToUnderscore(var1); expect(test1).to.equal('placement_id_test'); let var2 = 'my_test_value'; - let test2 = utils.convertCamelToUnderscore(var2); + let test2 = convertCamelToUnderscore(var2); expect(test2).to.equal(var2); }); }); - describe('getAdUnitSizes', function () { - it('returns an empty response when adUnits is undefined', function () { - let sizes = utils.getAdUnitSizes(); - expect(sizes).to.be.undefined; - }); - - it('returns an empty array when invalid data is present in adUnit object', function () { - let sizes = utils.getAdUnitSizes({ sizes: 300 }); - expect(sizes).to.deep.equal([]); - }); - - it('retuns an array of arrays when reading from adUnit.sizes', function () { - let sizes = utils.getAdUnitSizes({ sizes: [300, 250] }); - expect(sizes).to.deep.equal([[300, 250]]); - - sizes = utils.getAdUnitSizes({ sizes: [[300, 250], [300, 600]] }); - expect(sizes).to.deep.equal([[300, 250], [300, 600]]); - }); - - it('returns an array of arrays when reading from adUnit.mediaTypes.banner.sizes', function () { - let sizes = utils.getAdUnitSizes({ mediaTypes: { banner: { sizes: [300, 250] } } }); - expect(sizes).to.deep.equal([[300, 250]]); - - sizes = utils.getAdUnitSizes({ mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } } }); - expect(sizes).to.deep.equal([[300, 250], [300, 600]]); - }); - }); - describe('URL helpers', function () { describe('parseUrl()', function () { let parsed; @@ -1233,5 +1119,44 @@ describe('memoize', () => { mem('one', 'three'); expect(mem('one', 'three')).to.eql(['one', 'three']); expect(fn.callCount).to.eql(2); - }) + }); + + describe('binarySearch', () => { + [ + { + arr: [], + tests: [ + ['any', 0] + ] + }, + { + arr: [10], + tests: [ + [5, 0], + [10, 0], + [20, 1], + ], + }, + { + arr: [10, 20, 30, 30, 40], + tests: [ + [5, 0], + [15, 1], + [10, 0], + [30, 2], + [35, 4], + [40, 4], + [100, 5] + ] + } + ].forEach(({arr, tests}) => { + describe(`on ${arr}`, () => { + tests.forEach(([el, pos]) => { + it(`finds index for ${el} => ${pos}`, () => { + expect(binarySearch(arr, el)).to.equal(pos); + }); + }); + }); + }) + }); }) diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index c7c0b2eb329..c746fdd2afd 100644 --- a/test/spec/videoCache_spec.js +++ b/test/spec/videoCache_spec.js @@ -174,7 +174,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -224,7 +224,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -295,7 +295,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -356,7 +356,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); JSON.parse(request.requestBody).should.deep.equal({ puts: [{ diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 61621c7ec42..35d0a4fef24 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -1,4 +1,4 @@ -import { isValidVideoBid } from 'src/video.js'; +import {fillVideoDefaults, isValidVideoBid} from 'src/video.js'; import {hook} from '../../src/hook.js'; import {stubAuctionIndex} from '../helpers/indexStub.js'; @@ -7,97 +7,169 @@ describe('video.js', function () { hook.ready(); }); - it('validates valid instream bids', function () { - const bid = { - adId: '456xyz', - vastUrl: 'http://www.example.com/vastUrl', - transactionId: 'au' - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'instream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + describe('fillVideoDefaults', () => { + function fillDefaults(videoMediaType = {}) { + const adUnit = {mediaTypes: {video: videoMediaType}}; + fillVideoDefaults(adUnit); + return adUnit.mediaTypes.video; + } - it('catches invalid instream bids', function () { - const bid = { - transactionId: 'au' - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'instream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(false); - }); + describe('should set plcmt = 4 when', () => { + it('context is "outstream"', () => { + expect(fillDefaults({context: 'outstream'})).to.eql({ + context: 'outstream', + plcmt: 4 + }) + }); + [2, 3, 4].forEach(placement => { + it(`placemement is "${placement}"`, () => { + expect(fillDefaults({placement})).to.eql({ + placement, + plcmt: 4 + }); + }) + }); + }); + describe('should set plcmt = 2 when', () => { + [2, 6].forEach(playbackmethod => { + it(`playbackmethod is "${playbackmethod}"`, () => { + expect(fillDefaults({playbackmethod})).to.eql({ + playbackmethod, + plcmt: 2, + }); + }); + }); + }); + describe('should not set plcmt when', () => { + Object.entries({ + 'it was set by pub (context=outstream)': { + expected: 1, + video: { + context: 'outstream', + plcmt: 1 + } + }, + 'it was set by pub (placement=2)': { + expected: 1, + video: { + placement: 2, + plcmt: 1 + } + }, + 'placement not in 2, 3, 4': { + expected: undefined, + video: { + placement: 1 + } + }, + 'it was set by pub (playbackmethod=2)': { + expected: 1, + video: { + plcmt: 1, + playbackmethod: 2 + } + } + }).forEach(([t, {expected, video}]) => { + it(t, () => { + expect(fillDefaults(video).plcmt).to.eql(expected); + }) + }) + }) + }) - it('catches invalid bids when prebid-cache is disabled', function () { - const adUnits = [{ - transactionId: 'au', - bidder: 'vastOnlyVideoBidder', - mediaTypes: {video: {}}, - }]; + describe('isValidVideoBid', () => { + it('validates valid instream bids', function () { + const bid = { + adId: '456xyz', + vastUrl: 'http://www.example.com/vastUrl', + transactionId: 'au' + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'instream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); - const valid = isValidVideoBid({ transactionId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); + it('catches invalid instream bids', function () { + const bid = { + transactionId: 'au' + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'instream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(false); + }); - expect(valid).to.equal(false); - }); + it('catches invalid bids when prebid-cache is disabled', function () { + const adUnits = [{ + transactionId: 'au', + bidder: 'vastOnlyVideoBidder', + mediaTypes: {video: {}}, + }]; - it('validates valid outstream bids', function () { - const bid = { - transactionId: 'au', - renderer: { - url: 'render.url', - render: () => true, - } - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + const valid = isValidVideoBid({ transactionId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); - it('validates valid outstream bids with a publisher defined renderer', function () { - const bid = { - transactionId: 'au', - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: { - context: 'outstream', + expect(valid).to.equal(false); + }); + + it('validates valid outstream bids', function () { + const bid = { + transactionId: 'au', + renderer: { + url: 'render.url', + render: () => true, } - }, - renderer: { - url: 'render.url', - render: () => true, - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); - it('catches invalid outstream bids', function () { - const bid = { - transactionId: 'au', - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(false); - }); + it('validates valid outstream bids with a publisher defined renderer', function () { + const bid = { + transactionId: 'au', + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: { + context: 'outstream', + } + }, + renderer: { + url: 'render.url', + render: () => true, + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); + + it('catches invalid outstream bids', function () { + const bid = { + transactionId: 'au', + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(false); + }); + }) }); diff --git a/test/test_deps.js b/test/test_deps.js index 35713106f8c..c8a3bcc9426 100644 --- a/test/test_deps.js +++ b/test/test_deps.js @@ -4,6 +4,16 @@ window.process = { } }; +window.addEventListener('error', function (ev) { + // eslint-disable-next-line no-console + console.error('Uncaught exception:', ev.error, ev.error?.stack); +}) + +window.addEventListener('unhandledrejection', function (ev) { + // eslint-disable-next-line no-console + console.error('Unhandled rejection:', ev.reason); +}) + require('test/helpers/consentData.js'); require('test/helpers/prebidGlobal.js'); require('test/mocks/adloaderStub.js'); From 63542c129c2dc60b6bc2368fa0b30886d9be481f Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 22 Nov 2023 07:49:49 +0100 Subject: [PATCH 07/10] Don't append url on every buildrequest --- modules/conceptxBidAdapter.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index 1ccddd32748..87ac96f2131 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -3,7 +3,7 @@ import { BANNER } from '../src/mediaTypes.js'; // import { logError, logInfo, logWarn, parseUrl } from '../src/utils.js'; const BIDDER_CODE = 'conceptx'; -let ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; +const ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; // const LOG_PREFIX = 'ConceptX: '; export const spec = { @@ -16,10 +16,10 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { // logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); const requests = []; - + let requestUrl = `${ENDPOINT_URL}` if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - ENDPOINT_URL += '?gdpr_applies=' + bidderRequest.gdprConsent.gdprApplies; - ENDPOINT_URL += '&consentString=' + bidderRequest.gdprConsent.consentString; + requestUrl += '?gdpr_applies=' + bidderRequest.gdprConsent.gdprApplies; + requestUrl += '&consentString=' + bidderRequest.gdprConsent.consentString; } for (var i = 0; i < validBidRequests.length; i++) { const requestParent = { adUnits: [], meta: {} }; @@ -33,7 +33,7 @@ export const spec = { requestParent.adUnits.push(adUnit); requests.push({ method: 'POST', - url: ENDPOINT_URL, + url: requestUrl, options: { withCredentials: false, }, From 558f6611739934de2e8483731718d278199b4335 Mon Sep 17 00:00:00 2001 From: cpcpn-emil <115714010+cpcpn-emil@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:23:47 +0100 Subject: [PATCH 08/10] Add gvlId to conceptX --- modules/conceptxBidAdapter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index 87ac96f2131..47c50a4c0ad 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -5,10 +5,12 @@ import { BANNER } from '../src/mediaTypes.js'; const BIDDER_CODE = 'conceptx'; const ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; // const LOG_PREFIX = 'ConceptX: '; +const GVLID = 1340; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], + gvlid: GVLID, isBidRequestValid: function (bid) { return !!(bid.bidId && bid.params.site && bid.params.adunit); }, From fe4e9339a3761ef5957ed8fe587d7ffff8b4571f Mon Sep 17 00:00:00 2001 From: Emilhein Date: Fri, 6 Feb 2026 13:10:22 +0100 Subject: [PATCH 09/10] Change conceptx adapter, to directly request our PBS --- modules/conceptxBidAdapter.js | 268 +++++++++++++++---- test/spec/modules/conceptxBidAdapter_spec.js | 212 +++++++++------ 2 files changed, 352 insertions(+), 128 deletions(-) diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index 67ebd88e4e4..0ffca64a6d7 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -1,81 +1,247 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -// import { logError, logInfo, logWarn, parseUrl } from '../src/utils.js'; const BIDDER_CODE = 'conceptx'; -const ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; -// const LOG_PREFIX = 'ConceptX: '; +const ENDPOINT_URL = 'https://cxba-s2s.cncpt.dk/openrtb2/auction'; const GVLID = 1340; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], gvlid: GVLID, + isBidRequestValid: function (bid) { - return !!(bid.bidId && bid.params.site && bid.params.adunit); + return !!(bid.bidId && bid.params && bid.params.adunit); }, buildRequests: function (validBidRequests, bidderRequest) { - // logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); const requests = []; - let requestUrl = `${ENDPOINT_URL}` - if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - requestUrl += '?gdpr_applies=' + bidderRequest.gdprConsent.gdprApplies; - requestUrl += '&consentString=' + bidderRequest.gdprConsent.consentString; - } - for (var i = 0; i < validBidRequests.length; i++) { - const requestParent = { adUnits: [], meta: {} }; - const bid = validBidRequests[i] - const { adUnitCode, auctionId, bidId, bidder, bidderRequestId, ortb2 } = bid - requestParent.meta = { adUnitCode, auctionId, bidId, bidder, bidderRequestId, ortb2 } - - const { site, adunit } = bid.params - const adUnit = { site, adunit, targetId: bid.bidId } - if (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) adUnit.dimensions = bid.mediaTypes.banner.sizes - requestParent.adUnits.push(adUnit); + + for (let i = 0; i < validBidRequests.length; i++) { + const bid = validBidRequests[i]; + const { + adUnitCode, + auctionId, + bidId, + bidder, + bidderRequestId, + ortb2 = {}, + } = bid; + const params = bid.params || {}; + + // PBS URL + GDPR query params + let url = ENDPOINT_URL; + const query = []; + + // Only add GDPR params when gdprApplies is explicitly 0 or 1 + if (bidderRequest && bidderRequest.gdprConsent) { + let gdprApplies = bidderRequest.gdprConsent.gdprApplies; + if (typeof gdprApplies === 'boolean') { + gdprApplies = gdprApplies ? 1 : 0; + } + if (gdprApplies === 0 || gdprApplies === 1) { + query.push('gdpr_applies=' + gdprApplies); + if (bidderRequest.gdprConsent.consentString) { + query.push( + 'gdpr_consent=' + + encodeURIComponent(bidderRequest.gdprConsent.consentString) + ); + } + } + } + + if (query.length) { + url += '?' + query.join('&'); + } + + // site + const page = + params.site || (ortb2.site && ortb2.site.page) || ''; + const domain = + params.domain || (ortb2.site && ortb2.site.domain) || page; + + const site = { + id: domain || page || adUnitCode, + domain: domain || '', + page: page || '', + }; + + // banner sizes from mediaTypes.banner.sizes + const formats = []; + if ( + bid.mediaTypes && + bid.mediaTypes.banner && + bid.mediaTypes.banner.sizes + ) { + let sizes = bid.mediaTypes.banner.sizes; + if (sizes.length && typeof sizes[0] === 'number') { + sizes = [sizes]; + } + for (let j = 0; j < sizes.length; j++) { + const size = sizes[j]; + if (size && size.length === 2) { + formats.push({ w: size[0], h: size[1] }); + } + } + } + + const banner = formats.length ? { format: formats } : {}; + + // currency & timeout + let currency = 'DKK'; + if ( + bidderRequest && + bidderRequest.currency && + bidderRequest.currency.adServerCurrency + ) { + currency = bidderRequest.currency.adServerCurrency; + } + + const tmax = (bidderRequest && bidderRequest.timeout) || 500; + + // device + const ua = + typeof navigator !== 'undefined' && navigator.userAgent + ? navigator.userAgent + : 'Mozilla/5.0'; + const device = { ua }; + + // build OpenRTB request for PBS with stored requests + const ortbRequest = { + id: auctionId || bidId, + site, + device, + cur: [currency], + tmax, + imp: [ + { + id: bidId, + banner, + ext: { + prebid: { + storedrequest: { + id: params.adunit, + }, + }, + }, + }, + ], + ext: { + prebid: { + storedrequest: { + id: 'cx_global', + }, + custommeta: { + adUnitCode, + auctionId, + bidId, + bidder, + bidderRequestId, + }, + }, + }, + }; + + // GDPR in body + if (bidderRequest && bidderRequest.gdprConsent) { + let gdprAppliesBody = bidderRequest.gdprConsent.gdprApplies; + if (typeof gdprAppliesBody === 'boolean') { + gdprAppliesBody = gdprAppliesBody ? 1 : 0; + } + + if (!ortbRequest.user) ortbRequest.user = {}; + if (!ortbRequest.user.ext) ortbRequest.user.ext = {}; + + if (bidderRequest.gdprConsent.consentString) { + ortbRequest.user.ext.consent = + bidderRequest.gdprConsent.consentString; + } + + if (!ortbRequest.regs) ortbRequest.regs = {}; + if (!ortbRequest.regs.ext) ortbRequest.regs.ext = {}; + + if (gdprAppliesBody === 0 || gdprAppliesBody === 1) { + ortbRequest.regs.ext.gdpr = gdprAppliesBody; + } + } + + // user IDs -> user.ext.eids + if (bid.userIdAsEids && bid.userIdAsEids.length) { + if (!ortbRequest.user) ortbRequest.user = {}; + if (!ortbRequest.user.ext) ortbRequest.user.ext = {}; + ortbRequest.user.ext.eids = bid.userIdAsEids; + } + requests.push({ method: 'POST', - url: requestUrl, + url, options: { - withCredentials: false, + withCredentials: true, }, - data: JSON.stringify(requestParent), + data: JSON.stringify(ortbRequest), }); } return requests; }, - interpretResponse: function (serverResponse, bidRequest) { - const bidResponses = []; - const bidResponsesFromServer = serverResponse.body.bidResponses; - if (Array.isArray(bidResponsesFromServer) && bidResponsesFromServer.length === 0) { - return bidResponses - } - const firstBid = bidResponsesFromServer[0] - if (!firstBid) { - return bidResponses + interpretResponse: function (serverResponse, request) { + const body = + serverResponse && serverResponse.body ? serverResponse.body : {}; + + // PBS OpenRTB: seatbid[].bid[] + if ( + !body.seatbid || + !Array.isArray(body.seatbid) || + body.seatbid.length === 0 + ) { + return []; } - const firstSeat = firstBid.ads[0] - if (!firstSeat) { - return bidResponses + + const currency = body.cur || 'DKK'; + const bids = []; + + // recover referrer (site.page) from original request + let referrer = ''; + try { + if (request && request.data) { + const originalReq = + typeof request.data === 'string' + ? JSON.parse(request.data) + : request.data; + if (originalReq && originalReq.site && originalReq.site.page) { + referrer = originalReq.site.page; + } + } + } catch (_) {} + + for (let i = 0; i < body.seatbid.length; i++) { + const seatbid = body.seatbid[i]; + if (!seatbid.bid || !Array.isArray(seatbid.bid)) continue; + + for (let j = 0; j < seatbid.bid.length; j++) { + const b = seatbid.bid[j]; + + if (!b || typeof b.price !== 'number' || !b.adm) continue; + + bids.push({ + requestId: b.impid || b.id, + cpm: b.price, + width: b.w, + height: b.h, + creativeId: b.crid || b.id || '', + dealId: b.dealid || b.dealId || undefined, + currency, + netRevenue: true, + ttl: 300, + referrer, + ad: b.adm, + }); + } } - const bidResponse = { - requestId: firstSeat.requestId, - cpm: firstSeat.cpm, - width: firstSeat.width, - height: firstSeat.height, - creativeId: firstSeat.creativeId, - dealId: firstSeat.dealId, - currency: firstSeat.currency, - netRevenue: true, - ttl: firstSeat.ttl, - referrer: firstSeat.referrer, - ad: firstSeat.html - }; - bidResponses.push(bidResponse); - return bidResponses; + + return bids; }, +}; -} registerBidder(spec); diff --git a/test/spec/modules/conceptxBidAdapter_spec.js b/test/spec/modules/conceptxBidAdapter_spec.js index 8e9bd2f8cc0..02780045496 100644 --- a/test/spec/modules/conceptxBidAdapter_spec.js +++ b/test/spec/modules/conceptxBidAdapter_spec.js @@ -1,70 +1,75 @@ -// import or require modules necessary for the test, e.g.: -import { expect } from 'chai'; // may prefer 'assert' in place of 'expect' +import { expect } from 'chai'; import { spec } from 'modules/conceptxBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -// import { config } from 'src/config.js'; describe('conceptxBidAdapter', function () { - const URL = 'https://conceptx.cncpt-central.com/openrtb'; - - const ENDPOINT_URL = `${URL}`; - const ENDPOINT_URL_CONSENT = `${URL}?gdpr_applies=true&consentString=ihaveconsented`; + const ENDPOINT_URL = 'https://cxba-s2s.cncpt.dk/openrtb2/auction'; + const ENDPOINT_URL_CONSENT = + ENDPOINT_URL + '?gdpr_applies=1&gdpr_consent=ihaveconsented'; const adapter = newBidder(spec); const bidderRequests = [ { bidId: '123', bidder: 'conceptx', + adUnitCode: 'div-1', + auctionId: 'auc-1', params: { - site: 'example', - adunit: 'some-id-3' + site: 'example.com', + adunit: 'some-id-3', }, mediaTypes: { banner: { sizes: [[930, 180]], - } + }, }, - } - ] - - const singleBidRequest = { - bid: [ - { - bidId: '123', - } - ] - } + }, + ]; const serverResponse = { body: { - 'bidResponses': [ + id: 'resp-1', + cur: 'DKK', + seatbid: [ { - 'ads': [ + seat: 'conceptx', + bid: [ { - 'referrer': 'http://localhost/prebidpage_concept_bidder.html', - 'ttl': 360, - 'html': '

DUMMY

', - 'requestId': '214dfadd1f8826', - 'cpm': 46, - 'currency': 'DKK', - 'width': 930, - 'height': 180, - 'creativeId': 'FAKE-ID', - 'meta': { - 'mediaType': 'banner' - }, - 'netRevenue': true, - 'destinationUrls': { - 'destination': 'https://concept.dk' - } - } + id: 'bid-1', + impid: '123', + price: 46, + w: 930, + h: 180, + crid: 'FAKE-ID', + adm: '

DUMMY

', + }, ], - 'matchedAdCount': 1, - 'targetId': '214dfadd1f8826' - } - ] - } - } + }, + ], + }, + }; + + const requestPayload = { + data: JSON.stringify({ + id: 'auc-1', + site: { id: 'example.com', domain: 'example.com', page: 'example.com' }, + imp: [ + { + id: '123', + ext: { + prebid: { + storedrequest: { id: 'some-id-3' }, + }, + }, + }, + ], + ext: { + prebid: { + storedrequest: { id: 'cx_global' }, + }, + }, + }), + }; describe('inherited functions', function () { it('exists and is a function', function () { @@ -73,52 +78,105 @@ describe('conceptxBidAdapter', function () { }); describe('isBidRequestValid', function () { - it('should return true when required params found', function () { + it('should return true when bidId and params.adunit are present', function () { expect(spec.isBidRequestValid(bidderRequests[0])).to.equal(true); }); + + it('should return false when params.adunit is missing', function () { + expect( + spec.isBidRequestValid({ + bidId: '123', + bidder: 'conceptx', + params: { site: 'example' }, + }) + ).to.equal(false); + }); + + it('should return false when bidId is missing', function () { + expect( + spec.isBidRequestValid({ + bidder: 'conceptx', + params: { site: 'example', adunit: 'id-1' }, + }) + ).to.equal(false); + }); }); describe('buildRequests', function () { - it('Test requests', function () { - const request = spec.buildRequests(bidderRequests, {}); - expect(request.length).to.equal(1); - expect(request[0]).to.have.property('data'); - const bid = JSON.parse(request[0].data).adUnits[0] - expect(bid.site).to.equal('example'); - expect(bid.adunit).to.equal('some-id-3'); - expect(JSON.stringify(bid.dimensions)).to.equal(JSON.stringify([ - [930, 180]])); + it('should build OpenRTB request with stored requests', function () { + const requests = spec.buildRequests(bidderRequests, {}); + expect(requests).to.have.lengthOf(1); + expect(requests[0]).to.have.property('method', 'POST'); + expect(requests[0]).to.have.property('url', ENDPOINT_URL); + expect(requests[0]).to.have.property('data'); + + const payload = JSON.parse(requests[0].data); + expect(payload).to.have.property('site'); + expect(payload.site).to.have.property('id', 'example.com'); + expect(payload).to.have.property('imp'); + expect(payload.imp).to.have.lengthOf(1); + expect(payload.imp[0].ext.prebid.storedrequest).to.deep.equal({ + id: 'some-id-3', + }); + expect(payload.ext.prebid.storedrequest).to.deep.equal({ + id: 'cx_global', + }); + expect(payload.imp[0].banner.format).to.deep.equal([{ w: 930, h: 180 }]); + }); + + it('should include withCredentials in options', function () { + const requests = spec.buildRequests(bidderRequests, {}); + expect(requests[0].options).to.deep.include({ withCredentials: true }); }); }); describe('user privacy', function () { - it('should NOT send GDPR Consent data if gdprApplies equals undefined', function () { - const request = spec.buildRequests(bidderRequests, { gdprConsent: { gdprApplies: undefined, consentString: 'iDoNotConsent' } }); - expect(request.length).to.equal(1); - expect(request[0]).to.have.property('url') - expect(request[0].url).to.equal(ENDPOINT_URL); + it('should NOT add GDPR params to URL when gdprApplies is undefined', function () { + const requests = spec.buildRequests(bidderRequests, { + gdprConsent: { gdprApplies: undefined, consentString: 'iDoNotConsent' }, + }); + expect(requests[0].url).to.equal(ENDPOINT_URL); }); - it('should send GDPR Consent data if gdprApplies', function () { - const request = spec.buildRequests(bidderRequests, { gdprConsent: { gdprApplies: true, consentString: 'ihaveconsented' } }); - expect(request.length).to.equal(1); - expect(request[0]).to.have.property('url') - expect(request[0].url).to.equal(ENDPOINT_URL_CONSENT); + + it('should add gdpr_applies and gdpr_consent to URL when GDPR applies', function () { + const requests = spec.buildRequests(bidderRequests, { + gdprConsent: { gdprApplies: true, consentString: 'ihaveconsented' }, + }); + expect(requests[0].url).to.include('gdpr_applies=1'); + expect(requests[0].url).to.include('gdpr_consent=ihaveconsented'); }); }); describe('interpretResponse', function () { - it('should return valid response when passed valid server response', function () { - const interpretedResponse = spec.interpretResponse(serverResponse, singleBidRequest); - const ad = serverResponse.body.bidResponses[0].ads[0] - expect(interpretedResponse).to.have.lengthOf(1); - expect(interpretedResponse[0].cpm).to.equal(ad.cpm); - expect(interpretedResponse[0].width).to.equal(Number(ad.width)); - expect(interpretedResponse[0].height).to.equal(Number(ad.height)); - expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); - expect(interpretedResponse[0].currency).to.equal(ad.currency); - expect(interpretedResponse[0].netRevenue).to.equal(true); - expect(interpretedResponse[0].ad).to.equal(ad.html); - expect(interpretedResponse[0].ttl).to.equal(360); + it('should return valid bids from PBS seatbid format', function () { + const interpreted = spec.interpretResponse(serverResponse, requestPayload); + expect(interpreted).to.have.lengthOf(1); + expect(interpreted[0].requestId).to.equal('123'); + expect(interpreted[0].cpm).to.equal(46); + expect(interpreted[0].width).to.equal(930); + expect(interpreted[0].height).to.equal(180); + expect(interpreted[0].creativeId).to.equal('FAKE-ID'); + expect(interpreted[0].currency).to.equal('DKK'); + expect(interpreted[0].netRevenue).to.equal(true); + expect(interpreted[0].ad).to.equal('

DUMMY

'); + expect(interpreted[0].ttl).to.equal(300); + }); + + it('should return empty array when no seatbid', function () { + const emptyResponse = { body: { seatbid: [] } }; + expect(spec.interpretResponse(emptyResponse, {})).to.deep.equal([]); + }); + + it('should return empty array when seatbid is missing', function () { + const noSeatbid = { body: {} }; + expect(spec.interpretResponse(noSeatbid, {})).to.deep.equal([]); + }); + }); + + describe('getUserSyncs', function () { + it('should return empty array (sync handled by runPbsCookieSync)', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, [], {}, '', {}); + expect(syncs).to.deep.equal([]); }); }); }); From 23989f3d248237bba4fcb1178f919ed943f29b2b Mon Sep 17 00:00:00 2001 From: Emilhein Date: Mon, 23 Feb 2026 19:11:11 +0100 Subject: [PATCH 10/10] Add empty getUserSync, as the syncing will be delegated (runPbsCookieSync) --- modules/conceptxBidAdapter.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index 0ffca64a6d7..eef57e0aaa8 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -242,6 +242,17 @@ export const spec = { return bids; }, + + /** + * Cookie sync for conceptx is handled by the enrichment script's runPbsCookieSync, + * which calls https://cxba-s2s.cncpt.dk/cookie_sync with bidders. The PBS returns + * bidder_status with usersync URLs, and the script runs iframe/image syncs. + * The adapter does not return sync URLs here since those come from the cookie_sync + * endpoint, not the auction response. + */ + getUserSyncs: function () { + return []; + }, }; registerBidder(spec);