diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 016f4055216..962e057fbc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,8 @@ master branch. Pull requests must have 80% code coverage before beign considered for merge. Additional details about the process can be found [here](./PR_REVIEW.md). +There are more details available if you'd like to contribute a [bid adapter](https://docs.prebid.org/dev-docs/bidder-adaptor.html) or [analytics adapter](https://docs.prebid.org/dev-docs/integrate-with-the-prebid-analytics-api.html). + ## Issues [prebid.org](http://prebid.org/) contains documentation that may help answer questions you have about using Prebid.js. If you can't find the answer there, try searching for a similar issue on the [issues page](https://github.com/prebid/Prebid.js/issues). diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 6402fcbbbaa..9a57539a0cd 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -14,7 +14,7 @@ For modules and core platform updates, the initial reviewer should request an ad - Review for obvious errors or bad coding practice / use best judgement here. - If the change is a new feature / change to core prebid.js - review the change with a Tech Lead on the project and make sure they agree with the nature of change. - If the change results in needing updates to docs (such as public API change, module interface etc), add a label for "needs docs" and inform the submitter they must submit a docs PR to update the appropriate area of Prebid.org **before the PR can merge**. Help them with finding where the docs are located on prebid.org if needed. - - Below are some examples of bidder specific updates that should require docs update (in their dev-docs/bidders/bidder.md file): + - Below are some examples of bidder specific updates that should require docs update (in their dev-docs/bidders/BIDDER.md file): - If they support the GDPR consentManagement module and TCF1, add `gdpr_supported: true` - If they support the GDPR consentManagement module and TCF2, add `tcf2_supported: true` - If they support the US Privacy consentManagementUsp module, add `usp_supported: true` @@ -23,7 +23,7 @@ For modules and core platform updates, the initial reviewer should request an ad - If they support COPPA, add `coppa_supported: true` - If they support SChain, add `schain_supported: true` - If their bidder doesn't work well with safeframed creatives, add `safeframes_ok: false`. This will alert publishers to not use safeframed creatives when creating the ad server entries for their bidder. - - If they're a member of Prebid.org, add `prebid_member: true` + - If they're setting a deal ID in some scenarios, add `bidder_supports_deals: true` - If all above is good, add a `LGTM` comment and request 1 additional core member to review. - Once there is 2 `LGTM` on the PR, merge to master - Ask the submitter to add a PR for documentation if applicable. @@ -34,18 +34,17 @@ For modules and core platform updates, the initial reviewer should request an ad - Follow steps above for general review process. In addition, please verify the following: - Verify that bidder has submitted valid bid params and that bids are being received. - Verify that bidder is not manipulating the prebid.js auction in any way or doing things that go against the principles of the project. If unsure check with the Tech Lead. -- Verify that the bidder is being as efficient as possible, ideally not loading an external library, however if they do load a library it should be cached. - Verify that code re-use is being done properly and that changes introduced by a bidder don't impact other bidders. - If the adapter being submitted is an alias type, check with the bidder contact that is being aliased to make sure it's allowed. -- If the adapter is triggering any user syncs make sure they are using the user sync module in the Prebid.js core. -- Requests to the bidder should support HTTPS -- Responses from the bidder should be compressed (such as gzip, compress, deflate) -- Bid responses may not use JSONP: All requests must be AJAX with JSON responses -- Video openrtb params should be read from the ad unit when available; however bidder config can override the ad unit. -- All user-sync (aka pixel) activity must be registered via the provided functions -- Adapters may not use the $$PREBID_GLOBAL$$ variable -- All adapters must support the creation of multiple concurrent instances. This means, for example, that adapters cannot rely on mutable global variables. -- Adapters may not globally override or default the standard ad server targeting values: hb_adid, hb_bidder, hb_pb, hb_deal, or hb_size, hb_source, hb_format. +- All required global and bidder-adapter rules defined in the [Module Rules](https://docs.prebid.org/dev-docs/module-rules.html) must be followed. Please review these rules often - we depend on reviewers to enforce them. +- All bidder parameter conventions must be followed: + - Video params must be read from AdUnit.mediaTypes.video when available; however bidder config can override the ad unit. + - First party data must be read from [`fpd.context` and `fpd.user`](https://docs.prebid.org/dev-docs/publisher-api-reference.html#setConfig-fpd). + - 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 `getFloors()` function. + - Adapters cannot accept an schain parameter. Rather, they must look for the schain parameter at bidRequest.schain. + - The bidRequest page referrer must checked in addition to any bidder-specific parameter. + - If they're getting the COPPA flag, it must come from config.getConfig('coppa'); + - After a new adapter is approved, let the submitter know they may open a PR in the [headerbid-expert repository](https://github.com/prebid/headerbid-expert) to have their adapter recognized by the [Headerbid Expert extension](https://chrome.google.com/webstore/detail/headerbid-expert/cgfkddgbnfplidghapbbnngaogeldmop). The PR should be to the [bidder patterns file](https://github.com/prebid/headerbid-expert/blob/master/bidderPatterns.js), adding an entry with their adapter's name and the url the adapter uses to send and receive bid responses. ## Ticket Coordinator diff --git a/README.md b/README.md index b3f33b27aa5..44882570d89 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ As you make code changes, the bundles will be rebuilt and the page reloaded auto ## Contribute -Many SSPs, bidders, and publishers have contributed to this project. [60+ Bidders](https://github.com/prebid/Prebid.js/tree/master/src/adapters) are supported by Prebid.js. +Many SSPs, bidders, and publishers have contributed to this project. [Hundreds of bidders](https://github.com/prebid/Prebid.js/tree/master/src/adapters) are supported by Prebid.js. For guidelines, see [Contributing](./CONTRIBUTING.md). @@ -276,9 +276,7 @@ Our PR review process can be found [here](https://github.com/prebid/Prebid.js/tr ### Add a Bidder Adapter -To add a bidder adapter module, see the instructions in [How to add a bidder adaptor](http://prebid.org/dev-docs/bidder-adaptor.html). - -Please **do NOT load Prebid.js inside your adapter**. If you do this, we will reject or remove your adapter as appropriate. +To add a bidder adapter module, see the instructions in [How to add a bidder adapter](https://docs.prebid.org/dev-docs/bidder-adaptor.html). ### Code Quality diff --git a/integrationExamples/gpt/jwplayerRtdProvider_example.html b/integrationExamples/gpt/jwplayerRtdProvider_example.html new file mode 100644 index 00000000000..3791ab42137 --- /dev/null +++ b/integrationExamples/gpt/jwplayerRtdProvider_example.html @@ -0,0 +1,98 @@ + + + + + + + JW Player RTD Provider Example + + + + + + + + +
Div-1
+
+ +
+ + diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index 4acbe0aae31..9307bc91456 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -115,7 +115,7 @@ consentManagement: { cmpApi: 'iab', timeout: 1000, - allowAuctionWithoutConsent: true + defaultGdprScope: true }, // consentManagement: { // cmpApi: 'static', @@ -128,7 +128,7 @@ // } // } // }, - usersync: { + userSync: { userIds: [{ name: "unifiedId", params: { @@ -164,6 +164,18 @@ }, }, { + name: "merkleId", + params: { + ptk: '12345678-aaaa-bbbb-cccc-123456789abc', //Set your real merkle partner key here + pubid: 'EXAMPLE' //Set your real merkle publisher id here + }, + storage: { + type: "html5", + name: "merkleId", + expires: 30 + }, + + },{ name: "parrableId", params: { // change to Parrable Partner Client ID(s) you received from the Parrable Partners you are using diff --git a/karma.conf.maker.js b/karma.conf.maker.js index 712ef14caa1..8af216d6262 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -170,6 +170,16 @@ module.exports = function(codeCoverage, browserstack, watchMode, file) { plugins: plugins } + + // To ensure that, we are able to run single spec file + // here we are adding preprocessors, when file is passed + if (file) { + config.files.forEach((file) => { + config.preprocessors[file] = ['webpack', 'sourcemap']; + }); + delete config.preprocessors['test/test_index.js']; + } + setReporters(config, codeCoverage, browserstack); setBrowsers(config, browserstack); return config; diff --git a/modules/.submodules.json b/modules/.submodules.json index 58ab8798fa5..50d17fc5f6c 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -7,16 +7,19 @@ "britepoolIdSystem", "liveIntentIdSystem", "lotamePanoramaId", + "merkleIdSystem", "criteoIdSystem", "netIdSystem", "identityLinkIdSystem", - "sharedIdSystem" + "sharedIdSystem", + "intentIqIdSystem" ], "adpod": [ "freeWheelAdserverVideo", "dfpAdServerVideo" ], "rtdModule": [ - "browsiRtdProvider" + "browsiRtdProvider", + "jwplayerRtdProvider" ] } diff --git a/modules/adbutlerBidAdapter.js b/modules/adbutlerBidAdapter.js index 06c2eea5d67..10edd8ae3e3 100644 --- a/modules/adbutlerBidAdapter.js +++ b/modules/adbutlerBidAdapter.js @@ -9,7 +9,7 @@ const BIDDER_CODE = 'adbutler'; export const spec = { code: BIDDER_CODE, pageID: Math.floor(Math.random() * 10e6), - aliases: ['divreach'], + aliases: ['divreach', 'doceree'], isBidRequestValid: function (bid) { return !!(bid.params.accountID && bid.params.zoneID); diff --git a/modules/adheseBidAdapter.js b/modules/adheseBidAdapter.js index cd5a44f34d8..606e56c13d5 100644 --- a/modules/adheseBidAdapter.js +++ b/modules/adheseBidAdapter.js @@ -4,10 +4,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'adhese'; +const GVLID = 553; const USER_SYNC_BASE_URL = 'https://user-sync.adhese.com/iframe/user_sync.html'; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bid) { @@ -112,12 +114,15 @@ function mergeTargets(targets, target) { if (target) { Object.keys(target).forEach(function (key) { const val = target[key]; - const values = Array.isArray(val) ? val : [val]; - if (targets[key]) { - const distinctValues = values.filter(v => targets[key].indexOf(v) < 0); - targets[key].push.apply(targets[key], distinctValues); - } else { - targets[key] = values; + const dirtyValues = Array.isArray(val) ? val : [val]; + const values = dirtyValues.filter(v => v === 0 || v); + if (values.length > 0) { + if (targets[key]) { + const distinctValues = values.filter(v => targets[key].indexOf(v) < 0); + targets[key].push.apply(targets[key], distinctValues); + } else { + targets[key] = values; + } } }); } diff --git a/modules/aniviewBidAdapter.js b/modules/aniviewBidAdapter.js index f0fab83aeea..6b83c40897e 100644 --- a/modules/aniviewBidAdapter.js +++ b/modules/aniviewBidAdapter.js @@ -3,6 +3,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; const BIDDER_CODE = 'aniview'; +const GVLID = 780; const TTL = 600; function avRenderer(bid) { @@ -24,10 +25,28 @@ function avRenderer(bid) { } function newRenderer(bidRequest) { - let playerDomain = bidRequest && bidRequest.bidRequest && bidRequest.bidRequest.params && bidRequest.bidRequest.params.playerDomain ? bidRequest.bidRequest.params.playerDomain : 'player.aniview.com'; + let playerDomain = 'player.aniview.com'; + const config = {}; + + if (bidRequest && bidRequest.bidRequest && bidRequest.bidRequest.params) { + const params = bidRequest.bidRequest.params + + if (params.playerDomain) { + playerDomain = params.playerDomain; + } + + if (params.AV_PUBLISHERID) { + config.AV_PUBLISHERID = params.AV_PUBLISHERID; + } + + if (params.AV_CHANNELID) { + config.AV_CHANNELID = params.AV_CHANNELID; + } + } + const renderer = Renderer.install({ url: 'https://' + playerDomain + '/script/6.1/prebidRenderer.js', - config: {}, + config: config, loaded: false, }); @@ -252,7 +271,8 @@ function getUserSyncs(syncOptions, serverResponses) { export const spec = { code: BIDDER_CODE, - aliases: ['selectmediavideo'], + gvlid: GVLID, + aliases: ['avantisvideo', 'selectmediavideo'], supportedMediaTypes: [VIDEO], isBidRequestValid, buildRequests, diff --git a/modules/brightMountainMediaBidAdapter.js b/modules/brightMountainMediaBidAdapter.js new file mode 100644 index 00000000000..5a285be71c0 --- /dev/null +++ b/modules/brightMountainMediaBidAdapter.js @@ -0,0 +1,89 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const BIDDER_CODE = 'brightmountainmedia'; +const AD_URL = 'https://console.brightmountainmedia.com/hb/bid'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && bid.params.placement_id); + }, + + buildRequests: (validBidRequests, bidderRequest) => { + let winTop = window; + let location; + try { + location = new URL(bidderRequest.refererInfo.referer) + winTop = window.top; + } catch (e) { + location = winTop.location; + utils.logMessage(e); + }; + let placements = []; + let request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language : '', + 'secure': 1, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + if (bidderRequest) { + if (bidderRequest.gdprConsent) { + request.gdpr_consent = bidderRequest.gdprConsent.consentString || 'ALL' + request.gdpr_require = bidderRequest.gdprConsent.gdprApplies ? 1 : 0 + } + } + for (let i = 0; i < validBidRequests.length; i++) { + let bid = validBidRequests[i]; + let traff = bid.params.traffic || BANNER + let placement = { + placementId: bid.params.placement_id, + bidId: bid.bidId, + sizes: bid.mediaTypes[traff].sizes, + traffic: traff + }; + if (bid.schain) { + placement.schain = bid.schain; + } + placements.push(placement); + } + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + try { + serverResponse = serverResponse.body; + for (let i = 0; i < serverResponse.length; i++) { + let resItem = serverResponse[i]; + + response.push(resItem); + } + } catch (e) { + utils.logMessage(e); + }; + return response; + }, + + getUserSyncs: (syncOptions) => { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: 'https://console.brightmountainmedia.com:4444/cookieSync' + }]; + } + }, + +}; + +registerBidder(spec); diff --git a/modules/brightMountainMediaBidAdapter.md b/modules/brightMountainMediaBidAdapter.md new file mode 100644 index 00000000000..a9900b5264d --- /dev/null +++ b/modules/brightMountainMediaBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: Bright Mountain Media Bidder Adapter +Module Type: Bidder Adapter +Maintainer: dev@brightmountainmedia.com +``` + +# Description + +Connects to Bright Mountain Media exchange for bids. + +Bright Mountain Media bid adapter currently supports Banner. + +# Test Parameters +``` + var adUnits = [ + code: 'placementid_0', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'brightmountainmedia', + params: { + placement_id: '5f21784949be82079d08c', + traffic: 'banner' + } + } + ] + ]; +``` \ No newline at end of file diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 9317786fb8d..3aff3c6aac6 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -37,6 +37,8 @@ let _moduleParams = {}; let _data = null; /** @type {null | function} */ let _dataReadyCallback = null; +/** @type {string} */ +const DEF_KEYNAME = 'browsiViewability'; /** * add browsi script to page @@ -117,16 +119,14 @@ function waitForData(callback) { function sendDataToModule(adUnits, onDone) { try { waitForData(_predictionsData => { - const _predictions = _predictionsData.p; - if (!_predictions || !Object.keys(_predictions).length) { - return onDone({}); - } + const _predictions = _predictionsData.p || {}; let dataToReturn = adUnits.reduce((rp, cau) => { const adUnitCode = cau && cau.code; if (!adUnitCode) { return rp } const adSlot = getSlotByCode(adUnitCode); const identifier = adSlot ? getMacroId(_predictionsData.pmd, adSlot) : adUnitCode; const predictionData = _predictions[identifier]; + rp[adUnitCode] = getKVObject(-1, _predictionsData.kn); if (!predictionData) { return rp } if (predictionData.p) { @@ -160,7 +160,7 @@ function getAllSlots() { function getKVObject(p, keyName) { const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); let prObject = {}; - prObject[((_moduleParams['keyName'] || keyName).toString())] = prValue.toString(); + prObject[((_moduleParams['keyName'] || keyName || DEF_KEYNAME).toString())] = prValue.toString(); return prObject; } /** @@ -231,7 +231,7 @@ function evaluate(macro, divId, adUnit, replacer) { * @param {string} url server url with query params */ function getPredictionsFromServer(url) { - let ajax = ajaxBuilder(_moduleParams.auctionDelay || _moduleParams.timeout || DEF_TIMEOUT); + let ajax = ajaxBuilder(_moduleParams.auctionDelay || _moduleParams.timeout); ajax(url, { @@ -286,21 +286,26 @@ export const browsiSubmodule = { * @param {adUnit[]} adUnits * @param {function} onDone */ - getData: sendDataToModule + getData: sendDataToModule, + init: init }; -export function init(config) { +function init(config, gdpr, usp) { + return true; +} + +export function beforeInit(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { try { _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; + confListener(); _moduleParams.auctionDelay = realTimeData.auctionDelay; - _moduleParams.timeout = realTimeData.timeout; + _moduleParams.timeout = realTimeData.timeout || DEF_TIMEOUT; } catch (e) { _moduleParams = {}; } if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { - confListener(); collectData(); } else { utils.logError('missing params for Browsi provider'); @@ -308,5 +313,8 @@ export function init(config) { }); } -submodule('realTimeData', browsiSubmodule); -init(config); +function registerSubModule() { + submodule('realTimeData', browsiSubmodule); +} +registerSubModule(); +beforeInit(config); diff --git a/modules/concertAnalyticsAdapter.js b/modules/concertAnalyticsAdapter.js new file mode 100644 index 00000000000..a81d07e63b5 --- /dev/null +++ b/modules/concertAnalyticsAdapter.js @@ -0,0 +1,120 @@ +import {ajax} from '../src/ajax.js'; +import adapter from '../src/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager from '../src/adapterManager.js'; +import * as utils from '../src/utils.js'; + +const analyticsType = 'endpoint'; + +// We only want to send about 1% of events for sampling purposes +const SAMPLE_RATE_PERCENTAGE = 1 / 100; +const pageIncludedInSample = sampleAnalytics(); + +const url = 'https://bids.concert.io/analytics'; + +const { + EVENTS: { + BID_RESPONSE, + BID_WON, + AUCTION_END + } +} = CONSTANTS; + +let queue = []; + +let concertAnalytics = Object.assign(adapter({url, analyticsType}), { + track({ eventType, args }) { + switch (eventType) { + case BID_RESPONSE: + if (args.bidder !== 'concert') break; + queue.push(mapBidEvent(eventType, args)); + break; + + case BID_WON: + if (args.bidder !== 'concert') break; + queue.push(mapBidEvent(eventType, args)); + break; + + case AUCTION_END: + // Set a delay, as BID_WON events will come after AUCTION_END events + setTimeout(() => sendEvents(), 3000); + break; + + default: + break; + } + } +}); + +function mapBidEvent(eventType, args) { + const { adId, auctionId, cpm, creativeId, width, height, timeToRespond } = args; + const [gamCreativeId, concertRequestId] = getConcertRequestId(creativeId); + + const payload = { + event: eventType, + concert_rid: concertRequestId, + adId, + auctionId, + creativeId: gamCreativeId, + position: args.adUnitCode, + url: window.location.href, + cpm, + width, + height, + timeToRespond + } + + return payload; +} + +/** + * In order to pass back the concert_rid from CBS, it is tucked into the `creativeId` + * slot in the bid response and combined with a pipe `|`. This method splits the creative ID + * and the concert_rid. + * + * @param {string} creativeId + */ +function getConcertRequestId(creativeId) { + if (!creativeId || creativeId.indexOf('|') < 0) return [null, null]; + + return creativeId.split('|'); +} + +function sampleAnalytics() { + return Math.random() <= SAMPLE_RATE_PERCENTAGE; +} + +function sendEvents() { + concertAnalytics.eventsStorage = queue; + + if (!queue.length) return; + + if (!pageIncludedInSample) { + utils.logMessage('Page not included in sample for Concert Analytics'); + return; + } + + try { + const body = JSON.stringify(queue); + ajax(url, () => queue = [], body, { + contentType: 'application/json', + method: 'POST' + }); + } catch (err) { utils.logMessage('Concert Analytics error') } +} + +// save the base class function +concertAnalytics.originEnableAnalytics = concertAnalytics.enableAnalytics; +concertAnalytics.eventsStorage = []; + +// override enableAnalytics so we can get access to the config passed in from the page +concertAnalytics.enableAnalytics = function (config) { + concertAnalytics.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: concertAnalytics, + code: 'concert' +}); + +export default concertAnalytics; diff --git a/modules/concertAnalyticsAdapter.md b/modules/concertAnalyticsAdapter.md new file mode 100644 index 00000000000..7c9b6d22703 --- /dev/null +++ b/modules/concertAnalyticsAdapter.md @@ -0,0 +1,11 @@ +# Overview + +``` +Module Name: Concert Analytics Adapter +Module Type: Analytics Adapter +Maintainer: support@concert.io +``` + +# Description + +Analytics adapter for concert. \ No newline at end of file diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js new file mode 100644 index 00000000000..d153ddf9ee2 --- /dev/null +++ b/modules/concertBidAdapter.js @@ -0,0 +1,208 @@ + +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js' + +const BIDDER_CODE = 'concert'; +const CONCERT_ENDPOINT = 'https://bids.concert.io'; +const USER_SYNC_URL = 'https://cdn.concert.io/lib/bids/sync.html'; + +export const spec = { + code: BIDDER_CODE, + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + if (!bid.params.partnerId) { + utils.logWarn('Missing partnerId bid parameter'); + return false; + } + + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @param {bidderRequest} - + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + utils.logMessage(validBidRequests); + utils.logMessage(bidderRequest); + let payload = { + meta: { + prebidVersion: '$prebid.version$', + pageUrl: bidderRequest.refererInfo.referer, + screen: [window.screen.width, window.screen.height].join('x'), + debug: utils.debugTurnedOn(), + uid: getUid(bidderRequest), + optedOut: hasOptedOutOfPersonalization(), + adapterVersion: '1.1.0', + uspConsent: bidderRequest.uspConsent, + gdprConsent: bidderRequest.gdprConsent + } + } + + payload.slots = validBidRequests.map(bidRequest => { + let slot = { + name: bidRequest.adUnitCode, + bidId: bidRequest.bidId, + transactionId: bidRequest.transactionId, + sizes: bidRequest.sizes, + partnerId: bidRequest.params.partnerId, + slotType: bidRequest.params.slotType + } + + return slot; + }); + + utils.logMessage(payload); + + return { + method: 'POST', + url: `${CONCERT_ENDPOINT}/bids/prebid`, + data: JSON.stringify(payload) + } + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + utils.logMessage(serverResponse); + utils.logMessage(bidRequest); + + const serverBody = serverResponse.body; + + if (!serverBody || typeof serverBody !== 'object') { + return []; + } + + let bidResponses = []; + + bidResponses = serverBody.bids.map(bid => { + return { + requestId: bid.bidId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + ad: bid.ad, + ttl: bid.ttl, + creativeId: bid.creativeId, + netRevenue: bid.netRevenue, + currency: bid.currency + } + }); + + if (utils.debugTurnedOn() && serverBody.debug) { + utils.logMessage(`CONCERT`, serverBody.debug); + } + + utils.logMessage(bidResponses); + return bidResponses; + }, + + /** + * 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. + * @param {gdprConsent} object GDPR consent object. + * @param {uspConsent} string US Privacy String. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + if (syncOptions.iframeEnabled && !hasOptedOutOfPersonalization()) { + let params = []; + + if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean')) { + params.push(`gdpr_applies=${gdprConsent.gdprApplies ? '1' : '0'}`); + } + if (gdprConsent && (typeof gdprConsent.consentString === 'string')) { + params.push(`gdpr_consent=${gdprConsent.consentString}`); + } + if (uspConsent && (typeof uspConsent === 'string')) { + params.push(`usp_consent=${uspConsent}`); + } + + syncs.push({ + type: 'iframe', + url: USER_SYNC_URL + (params.length > 0 ? `?${params.join('&')}` : '') + }); + } + return syncs; + }, + + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ + onTimeout: function(data) { + utils.logMessage('concert bidder timed out'); + utils.logMessage(data); + }, + + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ + onBidWon: function(bid) { + utils.logMessage('concert bidder won bid'); + utils.logMessage(bid); + } + +} + +registerBidder(spec); + +const storage = getStorageManager(); + +/** + * Check or generate a UID for the current user. + */ +function getUid(bidderRequest) { + if (hasOptedOutOfPersonalization() || !consentAllowsPpid(bidderRequest)) { + return false; + } + + const CONCERT_UID_KEY = 'c_uid'; + + let uid = storage.getDataFromLocalStorage(CONCERT_UID_KEY); + + if (!uid) { + uid = utils.generateUUID(); + storage.setDataInLocalStorage(CONCERT_UID_KEY, uid); + } + + return uid; +} + +/** + * Whether the user has opted out of personalization. + */ +function hasOptedOutOfPersonalization() { + const CONCERT_NO_PERSONALIZATION_KEY = 'c_nap'; + + return storage.getDataFromLocalStorage(CONCERT_NO_PERSONALIZATION_KEY) === 'true'; +} + +/** + * Whether the privacy consent strings allow personalization. + * + * @param {BidderRequest} bidderRequest Object which contains any data consent signals + */ +function consentAllowsPpid(bidderRequest) { + /* NOTE: We cannot easily test GDPR consent, without the + * `consent-string` npm module; so will have to rely on that + * happening on the bid-server. */ + return !(bidderRequest.uspConsent === 'string' && + bidderRequest.uspConsent.toUpperCase().substring(0, 2) === '1YY') +} diff --git a/modules/concertBidAdapter.md b/modules/concertBidAdapter.md new file mode 100644 index 00000000000..faf774946d1 --- /dev/null +++ b/modules/concertBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: Concert Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@concert.io +``` + +# Description + +Module that connects to Concert demand sources + +# Test Paramters +``` + var adUnits = [ + { + code: 'desktop_leaderboard_variable', + mediaTypes: { + banner: { + sizes: [[1030, 590]] + } + } + bids: [ + { + bidder: "concert", + params: { + partnerId: 'test_partner' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js index 8b34df563ec..04459ec1f09 100644 --- a/modules/connectadBidAdapter.js +++ b/modules/connectadBidAdapter.js @@ -2,6 +2,7 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' import {config} from '../src/config.js'; +import {createEidsArray} from './userId/eids.js'; const BIDDER_CODE = 'connectad'; const BIDDER_CODE_ALIAS = 'connectadrealtime'; @@ -18,8 +19,6 @@ export const spec = { }, buildRequests: function(validBidRequests, bidderRequest) { - let digitrust; - let ret = { method: 'POST', url: '', @@ -41,7 +40,8 @@ export const spec = { screensize: getScreenSize(), dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, language: navigator.language, - ua: navigator.userAgent + ua: navigator.userAgent, + pversion: '$prebid.version$' }); // coppa compliance @@ -69,80 +69,19 @@ export const spec = { utils.deepSetValue(data, 'user.ext.us_privacy', bidderRequest.uspConsent); } - // Digitrust Support - const bidRequestDigitrust = utils.deepAccess(validBidRequests[0], 'userId.digitrustid.data'); - if (bidRequestDigitrust && (!bidRequestDigitrust.privacy || !bidRequestDigitrust.privacy.optout)) { - digitrust = { - id: bidRequestDigitrust.id, - keyv: bidRequestDigitrust.keyv - } - } - - if (digitrust) { - utils.deepSetValue(data, 'user.ext.digitrust', { - id: digitrust.id, - keyv: digitrust.keyv - }) - } - - if (validBidRequests[0].userId && typeof validBidRequests[0].userId === 'object' && (validBidRequests[0].userId.tdid || validBidRequests[0].userId.pubcid || validBidRequests[0].userId.lipb || validBidRequests[0].userId.id5id || validBidRequests[0].userId.parrableId)) { - utils.deepSetValue(data, 'user.ext.eids', []); - - if (validBidRequests[0].userId.tdid) { - data.user.ext.eids.push({ - source: 'adserver.org', - uids: [{ - id: validBidRequests[0].userId.tdid, - ext: { - rtiPartner: 'TDID' - } - }] - }); - } - - if (validBidRequests[0].userId.pubcid) { - data.user.ext.eids.push({ - source: 'pubcommon', - uids: [{ - id: validBidRequests[0].userId.pubcid, - }] - }); - } - - if (validBidRequests[0].userId.id5id) { - data.user.ext.eids.push({ - source: 'id5-sync.com', - uids: [{ - id: validBidRequests[0].userId.id5id, - }] - }); - } - - if (validBidRequests[0].userId.parrableId) { - data.user.ext.eids.push({ - source: 'parrable.com', - uids: [{ - id: validBidRequests[0].userId.parrableId.eid, - }] - }); - } - - if (validBidRequests[0].userId.lipb && validBidRequests[0].userId.lipb.lipbid) { - data.user.ext.eids.push({ - source: 'liveintent.com', - uids: [{ - id: validBidRequests[0].userId.lipb.lipbid - }] - }); - } + // EIDS Support + if (validBidRequests[0].userId) { + data.user.ext.eids = createEidsArray(validBidRequests[0].userId); } validBidRequests.map(bid => { const placement = Object.assign({ id: bid.transactionId, divName: bid.bidId, + pisze: bid.mediaTypes.banner.sizes[0] || bid.sizes[0], sizes: bid.mediaTypes.banner.sizes, - adTypes: getSize(bid.mediaTypes.banner.sizes || bid.sizes) + adTypes: getSize(bid.mediaTypes.banner.sizes || bid.sizes), + floor: getBidFloor(bid) }, bid.params); if (placement.networkId && placement.siteId) { @@ -277,6 +216,22 @@ sizeMap[331] = '320x250'; sizeMap[3301] = '320x267'; sizeMap[2730] = '728x250'; +function getBidFloor(bidRequest) { + let floorInfo = {}; + + if (typeof bidRequest.getFloor === 'function') { + floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'banner', + size: '*' + }); + } + + let floor = floorInfo.floor || 0; + + return floor; +} + function getSize(sizes) { const result = []; sizes.forEach(function(size) { diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 0c48d8c854c..90fe24735db 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -371,7 +371,7 @@ function cmpFailed(errMsg, hookConfig, extraArgs) { } /** - * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanger.js for later in the auction + * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() 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) */ function storeConsentData(cmpConsentObject) { @@ -387,6 +387,9 @@ function storeConsentData(cmpConsentObject) { vendorData: (cmpConsentObject) || undefined, gdprApplies: cmpConsentObject && typeof cmpConsentObject.gdprApplies === 'boolean' ? cmpConsentObject.gdprApplies : gdprScope }; + if (cmpConsentObject.addtlConsent && utils.isStr(cmpConsentObject.addtlConsent)) { + consentData.addtlConsent = cmpConsentObject.addtlConsent; + }; } consentData.apiVersion = cmpVersion; gdprDataHandler.setConsentData(consentData); diff --git a/modules/cpmstarBidAdapter.js b/modules/cpmstarBidAdapter.js old mode 100755 new mode 100644 index b416c00c2d0..61ccc0e786b --- a/modules/cpmstarBidAdapter.js +++ b/modules/cpmstarBidAdapter.js @@ -13,6 +13,12 @@ const ENDPOINT_PRODUCTION = 'https://server.cpmstar.com/view.aspx'; const DEFAULT_TTL = 300; const DEFAULT_CURRENCY = 'USD'; +function fixedEncodeURIComponent(str) { + return encodeURIComponent(str).replace(/[!'()*]/g, function(c) { + return '%' + c.charCodeAt(0).toString(16); + }); +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], @@ -47,13 +53,29 @@ export const spec = { 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) + '&reachedTop=' + encodeURIComponent(bidderRequest.refererInfo.reachedTop) + '&requestid=' + bidRequest.bidId + '&referer=' + encodeURIComponent(referer); + if (bidRequest.schain && bidRequest.schain.nodes) { + var schain = bidRequest.schain; + var schainString = ''; + schainString += schain.ver + ',' + schain.complete; + for (var i2 = 0; i2 < schain.nodes.length; i2++) { + var node = schain.nodes[i2]; + schainString += '!' + + fixedEncodeURIComponent(node.asi || '') + ',' + + fixedEncodeURIComponent(node.sid || '') + ',' + + fixedEncodeURIComponent(node.hp || '') + ',' + + fixedEncodeURIComponent(node.rid || '') + ',' + + fixedEncodeURIComponent(node.name || '') + ',' + + fixedEncodeURIComponent(node.domain || ''); + } + url += '&schain=' + schainString + } + if (bidderRequest.gdprConsent) { if (bidderRequest.gdprConsent.consentString != null) { url += '&gdpr_consent=' + bidderRequest.gdprConsent.consentString; @@ -138,6 +160,21 @@ export const spec = { } return bidResponses; + }, + + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + if (serverResponses.length == 0 || !serverResponses[0].body) return syncs; + var usersyncs = serverResponses[0].body[0].syncs; + if (!usersyncs || usersyncs.length < 0) return syncs; + for (var i = 0; i < usersyncs.length; i++) { + var us = usersyncs[i]; + if ((us.type === 'image' && syncOptions.pixelEnabled) || (us.type == 'iframe' && syncOptions.iframeEnabled)) { + syncs.push(us); + } + } + return syncs; } + }; registerBidder(spec); diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 1d999fdbacc..9fe8c26e4f6 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -42,7 +42,7 @@ import { auctionManager } from '../src/auctionManager.js'; const defaultParamConstants = { env: 'vp', gdfp_req: 1, - output: 'xml_vast3', + output: 'vast', unviewed_position_start: 1, }; diff --git a/modules/districtmDMXBidAdapter.js b/modules/districtmDMXBidAdapter.js index 03dca0b607d..21f3b7b3586 100644 --- a/modules/districtmDMXBidAdapter.js +++ b/modules/districtmDMXBidAdapter.js @@ -1,14 +1,26 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'districtmDMX'; const DMXURI = 'https://dmx.districtm.io/b/v1'; +const VIDEO_MAPPING = { + playback_method: { + 'auto_play_sound_on': 1, + 'auto_play_sound_off': 2, + 'click_to_play': 3, + 'mouse_over': 4, + 'viewport_sound_on': 5, + 'viewport_sound_off': 6 + } +}; export const spec = { code: BIDDER_CODE, - supportedFormat: ['banner'], + supportedFormat: [BANNER, VIDEO], + supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid(bid) { return !!(bid.params.dmxid && bid.params.memberid); }, @@ -27,14 +39,23 @@ export const spec = { nBid.requestId = nBid.impid; nBid.width = nBid.w || width; nBid.height = nBid.h || height; + nBid.mediaType = bid.mediaTypes && bid.mediaTypes.video ? 'video' : null; + if (nBid.mediaType) { + nBid.vastXml = cleanVast(nBid.adm); + } if (nBid.dealid) { nBid.dealId = nBid.dealid; } + nBid.uuid = nBid.bidId; nBid.ad = nBid.adm; nBid.netRevenue = true; nBid.creativeId = nBid.crid; nBid.currency = 'USD'; nBid.ttl = 60; + nBid.meta = nBid.meta || {}; + if (nBid.adomain && nBid.adomain.length > 0) { + nBid.meta.advertiserDomains = nBid.adomain; + } return nBid; } else { oBid.cpm = oBid.price; @@ -83,12 +104,19 @@ export const spec = { } let eids = []; - if (bidRequest && bidRequest.userId) { - bindUserId(eids, utils.deepAccess(bidRequest, `userId.idl_env`), 'liveramp.com', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.digitrustid.data.id`), 'digitru.st', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.id5id`), 'id5-sync.com', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.pubcid`), 'pubcid.org', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.tdid`), 'adserver.org', 1); + if (bidRequest[0] && bidRequest[0].userId) { + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.idl_env`), 'liveramp.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.id5id`), 'id5-sync.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.pubcid`), 'pubcid.org', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.tdid`), 'adserver.org', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.criteoId`), 'criteo.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.britepoolid`), 'britepool.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.lipb.lipbid`), 'liveintent.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.intentiqid`), 'intentiq.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.lotamePanoramaId`), 'lotame.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.parrableId`), 'parrable.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.netId`), 'netid.de', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.sharedid`), 'sharedid.org', 1); dmxRequest.user = dmxRequest.user || {}; dmxRequest.user.ext = dmxRequest.user.ext || {}; dmxRequest.user.ext.eids = eids; @@ -122,14 +150,34 @@ export const spec = { obj.id = dmx.bidId; obj.tagid = String(dmx.params.dmxid); obj.secure = 1; - obj.banner = { - topframe: 1, - w: cleanSizes(dmx.sizes, 'w'), - h: cleanSizes(dmx.sizes, 'h'), - format: cleanSizes(dmx.sizes).map(s => { - return {w: s[0], h: s[1]}; - }).filter(obj => typeof obj.w === 'number' && typeof obj.h === 'number') - }; + obj.bidfloor = getFloor(dmx); + if (dmx.mediaTypes && dmx.mediaTypes.video) { + obj.video = { + topframe: 1, + skip: dmx.mediaTypes.video.skippable || 0, + linearity: dmx.mediaTypes.video.linearity || 1, + minduration: dmx.mediaTypes.video.minduration || 5, + maxduration: dmx.mediaTypes.video.maxduration || 60, + playbackmethod: getPlaybackmethod(dmx.mediaTypes.video.playback_method), + api: getApi(dmx.mediaTypes.video), + mimes: dmx.mediaTypes.video.mimes || ['video/mp4'], + protocols: getProtocols(dmx.mediaTypes.video), + w: dmx.mediaTypes.video.playerSize[0][0], + h: dmx.mediaTypes.video.playerSize[0][1], + format: dmx.mediaTypes.video.playerSize.map(s => { + return {w: s[0], h: s[1]}; + }).filter(obj => typeof obj.w === 'number' && typeof obj.h === 'number') + }; + } else { + obj.banner = { + topframe: 1, + w: cleanSizes(dmx.sizes, 'w'), + h: cleanSizes(dmx.sizes, 'h'), + format: cleanSizes(dmx.sizes).map(s => { + return {w: s[0], h: s[1]}; + }).filter(obj => typeof obj.w === 'number' && typeof obj.h === 'number') + }; + } return obj; }); @@ -169,6 +217,27 @@ export const spec = { } } +export function getFloor (bid) { + let floor = null; + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: bid.mediaTypes.video ? 'video' : 'banner', + size: bid.sizes.map(size => { + return { + w: size[0], + h: size[1] + } + }) + }); + if (typeof floorInfo === 'object' && + floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + floor = parseFloat(floorInfo.floor); + } + } + return floor !== null ? floor : bid.params.floor; +} + export function cleanSizes(sizes, value) { const supportedSize = [ { @@ -310,4 +379,65 @@ export function bindUserId(eids, value, source, atype) { }) } } + +export function getApi({protocols}) { + let defaultValue = [2]; + let listProtocols = [ + {key: 'VPAID_1_0', value: 1}, + {key: 'VPAID_2_0', value: 2}, + {key: 'MRAID_1', value: 3}, + {key: 'ORMMA', value: 4}, + {key: 'MRAID_2', value: 5}, + {key: 'MRAID_3', value: 6}, + ]; + if (protocols) { + return listProtocols.filter(p => { + return protocols.indexOf(p.key) !== -1; + }).map(p => p.value) + } else { + return defaultValue; + } +} +export function getPlaybackmethod(playback) { + if (Array.isArray(playback) && playback.length > 0) { + return playback.map(label => { + return VIDEO_MAPPING.playback_method[label] + }) + } + return [2] +} + +export function getProtocols({protocols}) { + let defaultValue = [2, 3, 5, 6, 7, 8]; + let listProtocols = [ + {key: 'VAST_1_0', value: 1}, + {key: 'VAST_2_0', value: 2}, + {key: 'VAST_3_0', value: 3}, + {key: 'VAST_1_0_WRAPPER', value: 4}, + {key: 'VAST_2_0_WRAPPER', value: 5}, + {key: 'VAST_3_0_WRAPPER', value: 6}, + {key: 'VAST_4_0', value: 7}, + {key: 'VAST_4_0_WRAPPER', value: 8} + ]; + if (protocols) { + return listProtocols.filter(p => { + return protocols.indexOf(p.key) !== -1 + }).map(p => p.value); + } else { + return defaultValue; + } +} + +export function cleanVast(str) { + const toberemove = /]*?src\s*=\s*['\"]([^'\"]*?)['\"][^>]*?>/ + const [img, url] = str.match(toberemove) + str = str.replace(toberemove, '') + if (img) { + if (url) { + const insrt = `` + str = str.replace('', `${insrt}`) + } + } + return str; +} registerBidder(spec); diff --git a/modules/districtmDmxBidAdapter.md b/modules/districtmDmxBidAdapter.md index 792cf2e7305..5d5dd2affe6 100644 --- a/modules/districtmDmxBidAdapter.md +++ b/modules/districtmDmxBidAdapter.md @@ -20,13 +20,14 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman ## Media Types * Banner - +* Video ## Bidder Parameters | Key | Scope | Type | Description | --- | --- | --- | --- | `dmxid` | Mandatory | Integer | Unique identifier of the placement, dmxid can be obtained in the district m Boost platform. | `memberid` | Mandatory | Integer | Unique identifier for your account, memberid can be obtained in the district m Boost platform. +| `floor` | Optional | float | Most placement can have floor set in our platform, but this can now be set on the request too. # Ad Unit Configuration Example @@ -48,6 +49,35 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman }]; ``` +# Ad Unit Configuration Example for video request + +```javascript + var videoAdUnit = { + code: 'video1', + sizes: [640,480], + mediaTypes: { video: {context: 'instream', //or 'outstream' + playerSize: [[640, 480]], + skipppable: true, + minduration: 5, + maxduration: 45, + playback_method: ['auto_play_sound_off', 'viewport_sound_off'], + mimes: ["application/javascript", + "video/mp4"], + + } }, + bids: [ + { + bidder: 'districtmDMX', + params: { + dmxid: '100001', + memberid: '100003', + } + } + + ] + }; +``` + # Ad Unit Configuration when COPPA is needed @@ -117,6 +147,35 @@ Our demand and adapter supports multiple sizes per placement, as such a single d }]; ``` +Our bidder only supports instream context at the moment and we strongly like to put the media types and setting in the ad unit settings. +If no value is set the default value will be applied. + +```javascript + var videoAdUnit = { + code: 'video1', + sizes: [640,480], + mediaTypes: { video: {context: 'instream', //or 'outstream' + playerSize: [[640, 480]], + skipppable: true, + minduration: 5, + maxduration: 45, + playback_method: ['auto_play_sound_off', 'viewport_sound_off'], + mimes: ["application/javascript", + "video/mp4"], + + } }, + bids: [ + { + bidder: 'districtmDMX', + params: { + dmxid: '250258', + memberid: '100600', + } + } + ] + }; +``` + ###### 4. Implementation Checking Once the bidder is live in your Prebid configuration you may confirm it is making requests to our end point by looking for requests to `https://dmx.districtm.io/b/v1`. diff --git a/modules/docereeBidAdapter.md b/modules/docereeBidAdapter.md new file mode 100644 index 00000000000..9e3d4bbe1b8 --- /dev/null +++ b/modules/docereeBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +Module Name: Doceree Bidder Adapter +Module Type: Bidder Adapter + +# Description + +Connects to Doceree demand source to fetch bids. +Please use ```doceree``` as the bidder code. + +# Test Parameters +``` + var adUnits = [ + { + code: 'desktop-banner-ad-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "doceree", + params: { + accountID: '167283', + zoneID: '445501', + domain: 'adbserver.doceree.com', + extra: { + tuid: '1234-abcd' + } + } + } + ] + }, + ]; +``` diff --git a/modules/emx_digitalBidAdapter.js b/modules/emx_digitalBidAdapter.js index 6688d15d8e9..fa58481548a 100644 --- a/modules/emx_digitalBidAdapter.js +++ b/modules/emx_digitalBidAdapter.js @@ -162,6 +162,7 @@ export const emxAdapter = { export const spec = { code: BIDDER_CODE, + gvlid: 183, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function (bid) { if (!bid || !bid.params) { @@ -279,12 +280,21 @@ export const spec = { } return emxBidResponses; }, - getUserSyncs: function (syncOptions) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { const syncs = []; if (syncOptions.iframeEnabled) { + let url = 'https://biddr.brealtime.com/check.html'; + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + // add 'gdpr' only if 'gdprApplies' is defined + if (typeof gdprConsent.gdprApplies === 'boolean') { + url += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + url += `?gdpr_consent=${gdprConsent.consentString}`; + } + } syncs.push({ type: 'iframe', - url: 'https://biddr.brealtime.com/check.html' + url: url }); } return syncs; diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index c516c06d11a..14c33329b2d 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -6,8 +6,8 @@ */ import * as utils from '../src/utils.js' -import {ajax} from '../src/ajax.js'; -import {submodule} from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; /** @type {Submodule} */ export const identityLinkSubmodule = { @@ -16,6 +16,11 @@ export const identityLinkSubmodule = { * @type {string} */ name: 'identityLink', + /** + * used to specify vendor id + * @type {number} + */ + gvlid: 97, /** * decode the stored id value for passing to bid requests * @function @@ -39,10 +44,15 @@ export const identityLinkSubmodule = { } const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; const gdprConsentString = hasGdpr ? consentData.consentString : ''; + const tcfPolicyV2 = utils.deepAccess(consentData, 'vendorData.tcfPolicyVersion') === 2; // use protocol relative urls for http or https - const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? '&ct=1&cv=' + gdprConsentString : ''}`; + if (hasGdpr && (!gdprConsentString || gdprConsentString === '')) { + utils.logInfo('Consent string is required to call envelope API.'); + return; + } + const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? (tcfPolicyV2 ? '&ct=4&cv=' : '&ct=1&cv=') + gdprConsentString : ''}`; let resp; - resp = function(callback) { + resp = function (callback) { // Check ats during callback so it has a chance to initialise. // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint if (window.ats) { @@ -60,7 +70,7 @@ export const identityLinkSubmodule = { } }; - return {callback: resp}; + return { callback: resp }; } }; // return envelope from third party endpoint @@ -83,7 +93,7 @@ function getEnvelope(url, callback) { callback(); } }; - ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true}); + ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); } submodule('userId', identityLinkSubmodule); diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index 220aed47e15..50ed7d5f57c 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -8,7 +8,7 @@ const CONSTANTS = { SYNC_ENDPOINT: 'https://k.r66net.com/GetUserSync', TIME_TO_LIVE: 300, DEFAULT_CURRENCY: 'EUR', - PREBID_VERSION: 2, + PREBID_VERSION: 4, METHOD: 'GET', INVIBES_VENDOR_ID: 436 }; @@ -39,9 +39,7 @@ export const spec = { }, getUserSyncs: function (syncOptions) { if (syncOptions.iframeEnabled) { - handlePostMessage(); const syncUrl = buildSyncUrl(); - return { type: 'iframe', url: syncUrl @@ -55,6 +53,7 @@ registerBidder(spec); // some state info is required: cookie info, unique user visit id const topWin = getTopMostWindow(); let invibes = topWin.invibes = topWin.invibes || {}; +invibes.purposes = invibes.purposes || [false, false, false, false, false, false, false, false, false, false]; let _customUserSync; function isBidRequestValid(bid) { @@ -89,13 +88,11 @@ function buildRequest(bidRequests, bidderRequest) { _customUserSync = _customUserSync || bidRequest.params.customUserSync; }); - invibes.visitId = invibes.visitId || generateRandomId(); + invibes.optIn = invibes.optIn || readGdprConsent(bidderRequest.gdprConsent); - cookieDomain = detectTopmostCookieDomain(); + invibes.visitId = invibes.visitId || generateRandomId(); invibes.noCookies = invibes.noCookies || invibes.getCookie('ivNoCookie'); - invibes.optIn = invibes.optIn || invibes.getCookie('ivOptIn') || readGdprConsent(bidderRequest.gdprConsent); - - initDomainId(invibes.domainOptions); + let lid = initDomainId(invibes.domainOptions); const currentQueryStringParams = parseQueryStringParams(); @@ -117,14 +114,16 @@ function buildRequest(bidRequests, bidderRequest) { width: topWin.innerWidth, height: topWin.innerHeight, - noc: !cookieDomain, oi: invibes.optIn, - kw: keywords + kw: keywords, + purposes: invibes.purposes.toString(), + + tc: invibes.gdpr_consent }; - if (invibes.dom.id) { - data.lId = invibes.dom.id; + if (lid) { + data.lId = lid; } const parametersToPassForward = 'videoaddebug,advs,bvci,bvid,istop,trybvid,trybvci'.split(','); @@ -278,6 +277,8 @@ function renderCreative(bidModel) { function getCappedCampaignsAsString() { const key = 'ivvcap'; + if (!invibes.optIn || !invibes.purposes[0]) { return ''; } + let loadData = function () { try { return JSON.parse(storage.getDataFromLocalStorage(key)) || {}; @@ -348,49 +349,48 @@ function buildSyncUrl() { return syncUrl; } -function handlePostMessage() { - try { - if (window.addEventListener) { - window.addEventListener('message', acceptPostMessage); - } - } catch (e) { } -} - -function acceptPostMessage(e) { - let msg = e.data || {}; - if (msg.ivbscd === 1) { - invibes.setCookie(msg.name, msg.value, msg.exdays, msg.domain); - } else if (msg.ivbscd === 2) { - invibes.dom.graduate(); - } -} - function readGdprConsent(gdprConsent) { if (gdprConsent && gdprConsent.vendorData) { + invibes.gdpr_consent = getVendorConsentData(gdprConsent.vendorData); + if (!gdprConsent.vendorData.gdprApplies || gdprConsent.vendorData.hasGlobalConsent) { + var index; + for (index = 0; index < invibes.purposes; ++index) { + invibes.purposes[index] = true; + } return 2; } let purposeConsents = getPurposeConsents(gdprConsent.vendorData); if (purposeConsents == null) { return 0; } - let properties = Object.keys(purposeConsents); - let purposeConsentsCounter = getPurposeConsentsCounter(gdprConsent.vendorData); + let purposesLength = getPurposeConsentsCounter(gdprConsent.vendorData); - if (properties.length < purposeConsentsCounter) { + if (purposeConsents instanceof Array) { + for (let i = 0; i < purposesLength && i < purposeConsents.length; i++) { + invibes.purposes[i] = !((purposeConsents[i] === false || purposeConsents[i] === 'false' || purposeConsents[i] == null)); + } + } else if (typeof purposeConsents === 'object' && purposeConsents !== null) { + let i = 0; + for (let prop in purposeConsents) { + if (i === purposesLength) { + break; + } + + if (purposeConsents.hasOwnProperty(prop)) { + invibes.purposes[i] = !((purposeConsents[prop] === false || purposeConsents[prop] === 'false' || purposeConsents[prop] == null)); + i++; + } + } + } else { return 0; } - for (let i = 0; i < purposeConsentsCounter; i++) { - if (!purposeConsents[properties[i]] || purposeConsents[properties[i]] === 'false') { return 0; } - } - + let invibesVendorId = CONSTANTS.INVIBES_VENDOR_ID.toString(10); let vendorConsents = getVendorConsents(gdprConsent.vendorData); - if (vendorConsents == null || vendorConsents[CONSTANTS.INVIBES_VENDOR_ID.toString(10)] == null) { - return 4; - } + if (vendorConsents == null || vendorConsents[invibesVendorId] == null) { return 4; } - if (vendorConsents[CONSTANTS.INVIBES_VENDOR_ID.toString(10)] === false) { return 0; } + if (vendorConsents[invibesVendorId] === false) { return 0; } return 2; } @@ -418,6 +418,13 @@ function getPurposeConsents(vendorData) { return null; } +function getVendorConsentData(vendorData) { + if (vendorData.purpose && vendorData.purpose.consents) { + if (vendorData.tcString != null) { return vendorData.tcString; } + } + return vendorData.consentData; +}; + function getVendorConsents(vendorData) { if (vendorData.vendor && vendorData.vendor.consents) { return vendorData.vendor.consents; @@ -443,55 +450,15 @@ invibes.Uid = { } }; -let cookieDomain; invibes.getCookie = function (name) { if (!storage.cookiesAreEnabled()) { return; } - let i, x, y; - let cookies = document.cookie.split(';'); - for (i = 0; i < cookies.length; i++) { - x = cookies[i].substr(0, cookies[i].indexOf('=')); - y = cookies[i].substr(cookies[i].indexOf('=') + 1); - x = x.replace(/^\s+|\s+$/g, ''); - if (x === name) { - return unescape(y); - } - } -}; -invibes.setCookie = function (name, value, exdays, domain) { - if (!storage.cookiesAreEnabled()) { return; } - let whiteListed = name == 'ivNoCookie' || name == 'IvbsCampIdsLocal'; - if (invibes.noCookies && !whiteListed && (exdays || 0) >= 0) { return; } - if (exdays > 365) { exdays = 365; } - domain = domain || cookieDomain; - let exdate = new Date(); - let exms = exdays * 24 * 60 * 60 * 1000; - exdate.setTime(exdate.getTime() + exms); - storage.setCookie(name, value, exdate.toUTCString(), undefined, domain); -}; + if (!invibes.optIn || !invibes.purposes[0]) { return; } -let detectTopmostCookieDomain = function () { - let testCookie = invibes.Uid.generate(); - let hostParts = location.hostname.split('.'); - if (hostParts.length === 1) { - return location.hostname; - } - for (let i = hostParts.length - 1; i >= 0; i--) { - let domain = '.' + hostParts.slice(i).join('.'); - invibes.setCookie(testCookie, testCookie, 1, domain); - let val = invibes.getCookie(testCookie); - if (val === testCookie) { - invibes.setCookie(testCookie, testCookie, -1, domain); - return domain; - } - } + return storage.getCookie(name); }; let initDomainId = function (options) { - if (invibes.dom) { return; } - - options = options || {}; - let cookiePersistence = { cname: 'ivbsdid', load: function () { @@ -499,75 +466,16 @@ let initDomainId = function (options) { try { return JSON.parse(str); } catch (e) { } - }, - save: function (obj) { - invibes.setCookie(this.cname, JSON.stringify(obj), 365); - } - }; - - let persistence = options.persistence || cookiePersistence; - let state; - let minHC = 2; - - let validGradTime = function (state) { - if (!state.cr) { return false; } - let min = 151 * 10e9; - if (state.cr < min) { - return false; } - let now = new Date().getTime(); - let age = now - state.cr; - let minAge = 24 * 60 * 60 * 1000; - return age > minAge; - }; - - state = persistence.load() || { - id: invibes.Uid.generate(), - cr: new Date().getTime(), - hc: 1, }; - if (state.id.match(/\./)) { - state.id = invibes.Uid.generate(); - } - - let graduate = function () { - if (!state.cr) { return; } - delete state.cr; - delete state.hc; - persistence.save(state); - setId(); - }; + options = options || {}; - let regenerateId = function () { - state.id = invibes.Uid.generate(); - persistence.save(state); - }; + var persistence = options.persistence || cookiePersistence; - let setId = function () { - invibes.dom = { - get id() { - return (!state.cr && invibes.optIn > 0) ? state.id : undefined; - }, - get tempId() { - return (invibes.optIn > 0) ? state.id : undefined; - }, - graduate: graduate, - regen: regenerateId - }; - }; + let state = persistence.load(); - if (state.cr && !options.noVisit) { - if (state.hc < minHC) { - state.hc++; - } - if ((state.hc >= minHC && validGradTime(state)) || options.skipGraduation) { - graduate(); - } - } - persistence.save(state); - setId(); - ivLogger.info('Did=' + invibes.dom.id); + return state ? (state.id || state.tempId) : undefined; }; let keywords = (function () { diff --git a/modules/ironsourceBidAdapter.js b/modules/ironsourceBidAdapter.js new file mode 100644 index 00000000000..34650d46a0f --- /dev/null +++ b/modules/ironsourceBidAdapter.js @@ -0,0 +1,234 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; + +const SUPPORTED_AD_TYPES = [VIDEO]; +const BIDDER_CODE = 'ironsource'; +const BIDDER_VERSION = '4.0.0'; +const TTL = 360; +const SELLER_ENDPOINT = 'https://hb.yellowblue.io/hb'; +const SUPPORTED_SYNC_METHODS = { + IFRAME: 'iframe', + PIXEL: 'pixel' +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: function(bidRequest) { + return !!(bidRequest.params.isOrg); + }, + buildRequests: function (bidRequests, bidderRequest) { + if (bidRequests.length === 0) { + return []; + } + + const requests = []; + + bidRequests.forEach(bid => { + requests.push(buildVideoRequest(bid, bidderRequest)); + }); + + return requests; + }, + interpretResponse: function({body}) { + const bidResponses = []; + + const bidResponse = { + requestId: body.requestId, + cpm: body.cpm, + width: body.width, + height: body.height, + creativeId: body.requestId, + currency: body.currency, + netRevenue: body.netRevenue, + ttl: TTL, + vastXml: body.vastXml, + mediaType: VIDEO + }; + + bidResponses.push(bidResponse); + + return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (syncOptions.iframeEnabled && response.body.userSyncURL) { + syncs.push({ + type: 'iframe', + url: response.body.userSyncURL + }); + } + if (syncOptions.pixelEnabled && utils.isArray(response.body.userSyncPixels)) { + const pixels = response.body.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }) + syncs.push(...pixels) + } + } + return syncs; + } +}; + +registerBidder(spec); + +/** + * Build the video request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function buildVideoRequest(bid, bidderRequest) { + const sellerParams = generateParameters(bid, bidderRequest); + return { + method: 'GET', + url: SELLER_ENDPOINT, + data: sellerParams + }; +} + +/** + * Get the the ad size from the bid + * @param bid {bid} + * @returns {Array} + */ +function getSizes(bid) { + if (utils.deepAccess(bid, 'mediaTypes.video.sizes')) { + return bid.mediaTypes.video.sizes[0]; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + return bid.sizes[0]; + } + return []; +} + +/** + * Get schain string value + * @param schainObject {Object} + * @returns {string} + */ +function getSupplyChain(schainObject) { + if (utils.isEmpty(schainObject)) { + return ''; + } + let scStr = `${schainObject.ver},${schainObject.complete}`; + schainObject.nodes.forEach((node) => { + scStr += '!'; + scStr += `${getEncodedValIfNotEmpty(node.asi)},`; + scStr += `${getEncodedValIfNotEmpty(node.sid)},`; + scStr += `${getEncodedValIfNotEmpty(node.hp)},`; + scStr += `${getEncodedValIfNotEmpty(node.rid)},`; + scStr += `${getEncodedValIfNotEmpty(node.name)},`; + scStr += `${getEncodedValIfNotEmpty(node.domain)}`; + }); + return scStr; +} + +/** + * Get encoded node value + * @param val {string} + * @returns {string} + */ +function getEncodedValIfNotEmpty(val) { + return !utils.isEmpty(val) ? encodeURIComponent(val) : ''; +} + +/** + * Get preferred user-sync method based on publisher configuration + * @param bidderCode {string} + * @returns {string} + */ +function getAllowedSyncMethod(filterSettings, bidderCode) { + const iframeConfigsToCheck = ['all', 'iframe']; + const pixelConfigToCheck = 'image'; + if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { + return SUPPORTED_SYNC_METHODS.IFRAME; + } + if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { + return SUPPORTED_SYNC_METHODS.PIXEL; + } +} + +/** + * Check if sync rule is supported + * @param syncRule {Object} + * @param bidderCode {string} + * @returns {boolean} + */ +function isSyncMethodAllowed(syncRule, bidderCode) { + if (!syncRule) { + return false; + } + const isInclude = syncRule.filter === 'include'; + const bidders = utils.isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; + return isInclude && utils.contains(bidders, bidderCode); +} + +/** + * Generate query parameters for the request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function generateParameters(bid, bidderRequest) { + const timeout = config.getConfig('bidderTimeout'); + const { syncEnabled, filterSettings } = config.getConfig('userSync'); + const [ width, height ] = getSizes(bid); + const { params } = bid; + const { bidderCode } = bidderRequest; + const domain = window.location.hostname; + + const requestParams = { + auction_start: utils.timestamp(), + ad_unit_code: utils.getBidIdParameter('adUnitCode', bid), + tmax: timeout, + width: width, + height: height, + publisher_id: params.isOrg, + floor_price: params.floorPrice, + ua: navigator.userAgent, + bid_id: utils.getBidIdParameter('bidId', bid), + bidder_request_id: utils.getBidIdParameter('bidderRequestId', bid), + transaction_id: utils.getBidIdParameter('transactionId', bid), + session_id: utils.getBidIdParameter('auctionId', bid), + publisher_name: domain, + site_domain: domain, + bidder_version: BIDDER_VERSION + }; + + if (syncEnabled) { + const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + requestParams.cs_method = allowedSyncMethod; + } + } + + if (bidderRequest.uspConsent) { + requestParams.us_privacy = bidderRequest.uspConsent; + } + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + requestParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + requestParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + + if (params.ifa) { + requestParams.ifa = params.ifa; + } + + if (bid.schain) { + requestParams.schain = getSupplyChain(bid.schain); + } + + if (bidderRequest && bidderRequest.refererInfo) { + requestParams.referrer = utils.deepAccess(bidderRequest, 'refererInfo.referer'); + requestParams.page_url = utils.deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href'); + } + + return requestParams; +} diff --git a/modules/ironsourceBidAdapter.md b/modules/ironsourceBidAdapter.md new file mode 100644 index 00000000000..2f9e38b69e8 --- /dev/null +++ b/modules/ironsourceBidAdapter.md @@ -0,0 +1,49 @@ +#Overview + +Module Name: IronSource Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: prebid-digital-brands@ironsrc.com + + +# Description + +Module that connects to IronSource's demand sources. + +The IronSource adapter requires setup and approval from the IronSource. Please reach out to prebid-digital-brands@ironsrc.com to create an IronSource account. + +The adapter supports Video(instream). For the integration, IronSource returns content as vastXML and requires the publisher to define the cache url in config passed to Prebid for it to be valid in the auction. + +# Bid Parameters +## Video + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `isOrg` | required | String | IronSource publisher Id provided by your IronSource representative | "56f91cd4d3e3660002000033" +| `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 +| `ifa` | optional | String | The ID for advertisers (also referred to as "IDFA") | "XXX-XXX" + +# Test Parameters +```javascript +var adUnits = [ + { + code: 'dfp-video-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + } + }, + bids: [{ + bidder: 'ironsource', + params: { + isOrg: '56f91cd4d3e3660002000033', // Required + floorPrice: 2.00, // Optional + ifa: 'XXX-XXX', // Optional + } + }] + } + ]; +``` diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js new file mode 100644 index 00000000000..b7c8879ed8e --- /dev/null +++ b/modules/jwplayerRtdProvider.js @@ -0,0 +1,209 @@ +/** + * This module adds the jwplayer provider to the Real Time Data module (rtdModule) + * The {@link module:modules/realTimeData} module is required + * The module will allow Ad Bidders to obtain JW Player's Video Ad Targeting information + * The module will fetch segments for the media ids present in the prebid config when the module loads. If any bid + * requests are made while the segments are being fetched, they will be blocked until all requests complete, or the + * timeout expires. + * @module modules/jwplayerRtdProvider + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js'; +import { config } from '../src/config.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import { logError } from '../src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; + +const SUBMODULE_NAME = 'jwplayer'; +let requestCount = 0; +let requestTimeout = 150; +const segCache = {}; +let resumeBidRequest; + +/** @type {RtdSubmodule} */ +export const jwplayerSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: SUBMODULE_NAME, + /** + * get data and send back to realTimeData module + * @function + * @param {adUnit[]} adUnits + * @param {function} onDone + */ + getData: getSegments, + init +}; + +config.getConfig('realTimeData', ({realTimeData}) => { + const providers = realTimeData.dataProviders; + const jwplayerProvider = providers && find(providers, pr => pr.name && pr.name.toLowerCase() === SUBMODULE_NAME); + const params = jwplayerProvider && jwplayerProvider.params; + if (!params) { + return; + } + const rtdModuleTimeout = params.auctionDelay || params.timeout; + requestTimeout = rtdModuleTimeout === undefined ? requestTimeout : Math.max(rtdModuleTimeout - 1, 0); + fetchTargetingInformation(params); +}); + +submodule('realTimeData', jwplayerSubmodule); + +function init(config, gdpr, usp) { + return true; +} + +export function fetchTargetingInformation(jwTargeting) { + const mediaIDs = jwTargeting.mediaIDs; + if (!mediaIDs) { + return; + } + mediaIDs.forEach(mediaID => { + fetchTargetingForMediaId(mediaID); + }); +} + +export function fetchTargetingForMediaId(mediaId) { + const ajax = ajaxBuilder(requestTimeout); + requestCount++; + ajax(`https://cdn.jwplayer.com/v2/media/${mediaId}`, { + success: function (response) { + try { + const data = JSON.parse(response); + if (!data) { + throw ('Empty response'); + } + + const playlist = data.playlist; + if (!playlist || !playlist.length) { + throw ('Empty playlist'); + } + + const jwpseg = playlist[0].jwpseg; + if (jwpseg) { + segCache[mediaId] = jwpseg; + } + } catch (err) { + logError(err); + } + onRequestCompleted(); + }, + error: function () { + logError('failed to retrieve targeting information'); + onRequestCompleted(); + } + }); +} + +function onRequestCompleted() { + requestCount--; + if (requestCount > 0) { + return; + } + + if (resumeBidRequest) { + resumeBidRequest(); + resumeBidRequest = null; + } +} + +function getSegments(adUnits, onDone) { + executeAfterPrefetch(() => { + const realTimeData = adUnits.reduce((data, adUnit) => { + const code = adUnit.code; + const vat = code && getTargetingForBid(adUnit); + if (!vat) { + return data; + } + + const { segments, mediaID } = vat; + const jwTargeting = {}; + if (segments && segments.length) { + jwTargeting.segments = segments; + } + + if (mediaID) { + const id = 'jw_' + mediaID; + jwTargeting.content = { + id + } + } + + data[code] = { + jwTargeting + }; + return data; + }, {}); + onDone(realTimeData); + }); +} + +function executeAfterPrefetch(callback) { + if (requestCount > 0) { + resumeBidRequest = callback; + } else { + callback(); + } +} + +/** + * Retrieves the targeting information pertaining to a bid request. + * @param bidRequest {object} - the bid which is passed to a prebid adapter for use in `buildRequests`. It must contain + * a jwTargeting property. + * @returns targetingInformation {object} nullable - contains the media ID as well as the jwpseg targeting segments + * found for the given bidRequest information + */ +export function getTargetingForBid(bidRequest) { + const jwTargeting = bidRequest.jwTargeting; + if (!jwTargeting) { + return null; + } + const playerID = jwTargeting.playerID; + let mediaID = jwTargeting.mediaID; + let segments = segCache[mediaID]; + if (segments) { + return { + segments, + mediaID + }; + } + + const player = getPlayer(playerID); + if (!player) { + return null; + } + + const item = mediaID ? find(player.getPlaylist(), item => item.mediaid === mediaID) : player.getPlaylistItem(); + if (!item) { + return null; + } + + mediaID = mediaID || item.mediaid; + segments = item.jwpseg; + if (segments && mediaID) { + segCache[mediaID] = segments; + } + + return { + segments, + mediaID + }; +} + +function getPlayer(playerID) { + const jwplayer = window.jwplayer; + if (!jwplayer) { + logError('jwplayer.js was not found on page'); + return; + } + + const player = jwplayer(playerID); + if (!player || !player.getPlaylist) { + logError('player ID did not match any players'); + return; + } + return player; +} diff --git a/modules/jwplayerRtdProvider.md b/modules/jwplayerRtdProvider.md new file mode 100644 index 00000000000..06a7f69f497 --- /dev/null +++ b/modules/jwplayerRtdProvider.md @@ -0,0 +1,96 @@ +The purpose of this Real Time Data Provider is to allow publishers to target against their JW Player media without +having to integrate with the VPB product. This prebid module makes JW Player's video ad targeting information accessible +to Bid Adapters. + +**Usage for Publishers:** + +Compile the JW Player RTD Provider into your Prebid build: + +`gulp build --modules=jwplayerRtdProvider` + +Publishers must register JW Player as a real time data provider by setting up a Prebid Config conformant to the +following structure: + +```javascript +const jwplayerDataProvider = { + name: "jwplayer" +}; + +pbjs.setConfig({ + ..., + realTimeData: { + dataProviders: [ + jwplayerDataProvider + ] + } +}); +``` + +In order to prefetch targeting information for certain media, include the media IDs in the `jwplayerDataProvider` var: + +```javascript +const jwplayerDataProvider = { + name: "jwplayer", + params: { + mediaIDs: ['abc', 'def', 'ghi', 'jkl'] + } +}; +``` +Lastly, include the content's media ID and/or the player's ID in the matching AdUnit: + +```javascript +const adUnit = { + code: '/19968336/prebid_native_example_1', + ... + jwTargeting: { + playerID: 'abcd', + mediaID: '1234' + } +}; + +pbjs.que.push(function() { + pbjs.addAdUnits([adUnit]); + pbjs.requestBids({ + ... + }); +}); +``` + +**Usage for Bid Adapters:** + +Implement the `buildRequests` function. When it is called, the `bidRequests` param will be an array of bids. +Each bid for which targeting information was found will conform to the following object structure: + +```javascript +{ + adUnitCode: 'xyz', + bidId: 'abc', + ... + realTimeData: { + ..., + jwTargeting: { + segments: ['123', '456'], + content: { + id: 'jw_abc123' + } + } + } +} +``` + +where: +- `segments` is an array of jwpseg targeting segments, of type string. +- `content` is an object containing metadata for the media. It may contain the following information: + - `id` is a unique identifier for the specific media asset. + +**Example:** + +To view an example: + +- in your cli run: + +`gulp serve --modules=jwplayerRtdProvider` + +- in your browser, navigate to: + +`http://localhost:9999/integrationExamples/gpt/jwplayerRtdProvider_example.html` diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 31c35f4afe3..03767efc135 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -65,7 +65,8 @@ export const spec = { let meta; if (adUnit.metadata && adUnit.metadata.landingPageDomain) { meta = { - clickUrl: adUnit.metadata.landingPageDomain + clickUrl: adUnit.metadata.landingPageDomain, + advertiserDomains: [adUnit.metadata.landingPageDomain] }; } bidResponses.push({ diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js new file mode 100644 index 00000000000..68eb95ddae3 --- /dev/null +++ b/modules/mediagoBidAdapter.js @@ -0,0 +1,362 @@ +/** + * gulp serve --modules=mediagoBidAdapter --nolint --notest + */ + +import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'mediago'; +// const PROTOCOL = window.document.location.protocol; +const ENDPOINT_URL = + // ((PROTOCOL === 'https:') ? 'https' : 'http') + + 'https://rtb-us.mediago.io/api/bid?tn='; +const TIME_TO_LIVE = 500; +// const ENDPOINT_URL = '/api/bid?tn='; +const storage = getStorageManager(); +let globals = {}; +let itemMaps = {}; + +/** + * 获取随机id + * @param {number} a random number from 0 to 15 + * @return {string} random number or random string + */ +function getRandomId( + a // placeholder +) { + return a // if the placeholder was passed, return + ? ( // a random number from 0 to 15 + a ^ // unless b is 8, + Math.random() * // in which case + 16 >> // a random number from + a / 4 // 8 to 11 + ).toString(16) // in hexadecimal + : ( // or otherwise a concatenated string: + [1e7] + // 10000000 + + 1e3 + // -1000 + + 4e3 + // -4000 + + 8e3 + // -80000000 + + 1e11 // -100000000000, + ).replace( // replacing + /[018]/g, // zeroes, ones, and eights with + getRandomId // random hex digits + ); +} + +/* ----- mguid:start ------ */ +const COOKIE_KEY_MGUID = '__mguid_'; + +/** + * 获取用户id + * @return {string} + */ +const getUserID = () => { + const i = storage.getCookie(COOKIE_KEY_MGUID); + + if (i === null) { + const uuid = utils.generateUUID(); + storage.setCookie(COOKIE_KEY_MGUID, uuid); + return uuid; + } + return i; +}; + +/* ----- mguid:end ------ */ + +/** + * 获取一个对象的某个值,如果没有则返回空字符串 + * @param {Object} obj 对象 + * @param {...string} keys 键名 + * @return {any} + */ +function getProperty(obj, ...keys) { + let o = obj; + + for (let key of keys) { + // console.log(key, o); + if (o && o[key]) { + o = o[key]; + } else { + return ''; + } + } + return o; +} + +/** + * 是不是移动设备或者平板 + * @return {boolean} + */ +function isMobileAndTablet() { + let check = false; + (function(a) { + let reg1 = new RegExp(['(android|bb\d+|meego)', + '.+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)', + '|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone', + '|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap', + '|windows ce|xda|xiino|android|ipad|playbook|silk' + ].join(''), 'i'); + let reg2 = new RegExp(['1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)', + '|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )', + '|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell', + '|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)', + '|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene', + '|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c', + '|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom', + '|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)', + '|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)', + '|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]', + '|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)', + '|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio', + '|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms', + '|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al', + '|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)', + '|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|', + 'v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)', + '|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-', + '|your|zeto|zte\-' + ].join(''), 'i'); + if (reg1.test(a) || + reg2.test(a.substr(0, 4))) { + check = true; + } + })(navigator.userAgent || navigator.vendor || window.opera); + return check; +} + +/** + * 将尺寸转为RTB识别的尺寸 + * + * @param {Array|Object} requestSizes 配置尺寸 + * @return {Object} + */ +function transformSizes(requestSizes) { + let sizes = []; + let sizeObj = {}; + + if (utils.isArray(requestSizes) && requestSizes.length === 2 && + !utils.isArray(requestSizes[0])) { + sizeObj.width = parseInt(requestSizes[0], 10); + sizeObj.height = parseInt(requestSizes[1], 10); + sizes.push(sizeObj); + } else if (typeof requestSizes === 'object') { + for (let i = 0; i < requestSizes.length; i++) { + let size = requestSizes[i]; + sizeObj = {}; + sizeObj.width = parseInt(size[0], 10); + sizeObj.height = parseInt(size[1], 10); + sizes.push(sizeObj); + } + } + + return sizes; +} + +/** + * 获取广告位配置 + * @param {Array} validBidRequests an an array of bids + * @param {Object} bidderRequest The master bidRequest object + * @return {Object} + */ +function getItems(validBidRequests, bidderRequest) { + let items = []; + items = validBidRequests.map((req, i) => { + let ret = {}; + let mediaTypes = getProperty(req, 'mediaTypes'); + + let sizes = transformSizes(getProperty(req, 'sizes')); + let matchSize; + + // 确认尺寸是否符合我们要求 + for (let size of sizes) { + if (size.width === 300 && size.height === 250) { + matchSize = size; + break; + } + } + + // if (mediaTypes.native) {} + // banner广告类型 + if (mediaTypes.banner) { + let id = '' + (i + 1); + ret = { + id: id, + // bidFloor: 0, // todo + banner: { + h: matchSize.height, + w: matchSize.width, + pos: 1, + } + }; + itemMaps[id] = { + req, + ret + }; + } + + return ret; + }); + return items; +} + +/** + * 获取rtb请求参数 + * + * @param {Array} validBidRequests an an array of bids + * @param {Object} bidderRequest The master bidRequest object + * @return {Object} + */ +function getParam(validBidRequests, bidderRequest) { + // console.log(validBidRequests, bidderRequest); + let isMobile = isMobileAndTablet() ? 1 : 0; + let isTest = 0; + let auctionId = getProperty(bidderRequest, 'auctionId') || getRandomId(); + let items = getItems(validBidRequests, bidderRequest); + + const domain = document.domain; + const location = utils.deepAccess(bidderRequest, 'refererInfo.referer'); + + if (items && items.length) { + let c = { + 'id': 'mgprebidjs_' + auctionId, + 'test': +isTest, + 'at': 1, + 'cur': ['USD'], + 'device': { + // 'dnt':0, + // 'devicetype':2, + 'js': 1, + 'os': navigator.platform || '', + 'ua': navigator.userAgent, + 'language': /en/.test(navigator.language) ? 'en' : navigator.language, + // 'geo':{ + // 'country':'USA' + // } + }, + 'user': { + 'id': getUserID() // todo + }, + 'site': { + 'name': domain, + 'domain': domain, + 'page': location, + 'ref': location, + 'mobile': isMobile, + 'cat': [], // todo + 'publisher': { // todo + 'id': domain, + 'name': domain + } + }, + 'imp': items + }; + return c; + } else { + return null; + } +} + +export const spec = { + code: BIDDER_CODE, + // aliases: ['ex'], // short code + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + // console.log('mediago', { + // bid + // }); + if (bid.params.token) { + globals['token'] = bid.params.token; + } + return !!(bid.params.token); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {Array} validBidRequests an an array of bids + * @param {Object} bidderRequest The master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + let payload = getParam(validBidRequests, bidderRequest); + + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: ENDPOINT_URL + globals['token'], + data: payloadString, + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + const bids = getProperty(serverResponse, 'body', 'seatbid', 0, 'bid'); + const cur = getProperty(serverResponse, 'body', 'cur'); + + const bidResponses = []; + + for (let bid of bids) { + let impid = getProperty(bid, 'impid'); + if (itemMaps[impid]) { + let bidId = getProperty(itemMaps[impid], 'req', 'bidId'); + const bidResponse = { + requestId: bidId, + cpm: getProperty(bid, 'price'), + width: getProperty(bid, 'w'), + height: getProperty(bid, 'h'), + creativeId: getProperty(bid, 'crid'), + dealId: '', + currency: cur, + netRevenue: true, + ttl: TIME_TO_LIVE, + // referrer: REFERER, + ad: getProperty(bid, 'adm') + }; + bidResponses.push(bidResponse); + } + } + + return bidResponses; + }, + + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ + onTimeout: function(data) { + // console.log('onTimeout', data); + // Bidder specifc code + }, + + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ + onBidWon: function(bid) { + // console.log('onBidWon', bid); + // Bidder specific code + }, + + /** + * Register bidder specific code, which will execute when the adserver targeting has been set for a bid from this bidder + * @param {Bid} The bid of which the targeting has been set + */ + onSetTargeting: function(bid) { + // console.log('onSetTargeting', bid); + // Bidder specific code + } +}; +registerBidder(spec); diff --git a/modules/mediagoBidAdapter.md b/modules/mediagoBidAdapter.md new file mode 100644 index 00000000000..87c38f662a3 --- /dev/null +++ b/modules/mediagoBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: MediaGo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: fangsimin@baidu.com +``` + +# Description + +Module that connects to MediaGo's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: "mediago", + params: { + token: '' // required, send email to ext_mediago_am@baidu.com to get the corresponding token + } + } + ] + } + ]; +``` diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js new file mode 100644 index 00000000000..d6bf96618df --- /dev/null +++ b/modules/merkleIdSystem.js @@ -0,0 +1,80 @@ +/** + * This module adds merkleId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/merkleIdSystem + * @requires module:modules/userId + */ + +import * as utils from '../src/utils.js' +import {ajax} from '../src/ajax.js'; +import {submodule} from '../src/hook.js' + +const MODULE_NAME = 'merkleId'; + +/** @type {Submodule} */ +export const merkleIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{merkleId:string}} + */ + decode(value) { + const id = (value && value.ppid && typeof value.ppid.id === 'string') ? value.ppid.id : undefined; + return id ? { 'merkleId': id } : undefined; + }, + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleParams} [configParams] + * @param {ConsentData} [consentData] + * @returns {IdResponse|undefined} + */ + getId(configParams, consentData) { + if (!configParams || typeof configParams.pubid !== 'string') { + utils.logError('User ID - merkleId submodule requires a valid pubid to be defined'); + return; + } + + if (typeof configParams.ptk !== 'string') { + utils.logError('User ID - merkleId submodule requires a valid ptk string to be defined'); + return; + } + + if (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) { + utils.logError('User ID - merkleId submodule does not currently handle consent strings'); + return; + } + + const url = `https://mid.rkdms.com/idsv2?ptk=${configParams.ptk}&pubid=${configParams.pubid}`; + + const resp = function (callback) { + const callbacks = { + success: response => { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + utils.logError(error); + } + } + callback(responseObj); + }, + error: error => { + utils.logError(`${MODULE_NAME}: merkleId fetch encountered an error`, error); + callback(); + } + }; + ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true}); + }; + return {callback: resp}; + } +}; + +submodule('userId', merkleIdSubmodule); diff --git a/modules/oneVideoBidAdapter.js b/modules/oneVideoBidAdapter.js index 26b0a7dccc2..c5bc054ac04 100644 --- a/modules/oneVideoBidAdapter.js +++ b/modules/oneVideoBidAdapter.js @@ -4,12 +4,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'oneVideo'; export const spec = { code: 'oneVideo', - VERSION: '3.0.3', + VERSION: '3.0.4', ENDPOINT: 'https://ads.adaptv.advertising.com/rtb/openrtb?ext_id=', E2ETESTENDPOINT: 'https://ads-wc.v.ssp.yahoo.com/rtb/openrtb?ext_id=', - SYNC_ENDPOINT1: 'https://cm.g.doubleclick.net/pixel?google_nid=adaptv_dbm&google_cm&google_sc', - SYNC_ENDPOINT2: 'https://pr-bh.ybp.yahoo.com/sync/adaptv_ortb/{combo_uid}', - SYNC_ENDPOINT3: 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=adaptv&ttd_tpi=1', + SYNC_ENDPOINT1: 'https://pixel.advertising.com/ups/57304/sync?gdpr=&gdpr_consent=&_origin=0&redir=true', + SYNC_ENDPOINT2: 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=adaptv&ttd_tpi=1', supportedMediaTypes: ['video', 'banner'], /** * Determines whether or not the given bid request is valid. @@ -136,17 +135,13 @@ export const spec = { type: 'image', url: spec.SYNC_ENDPOINT1 }, - { - type: 'image', - url: spec.SYNC_ENDPOINT2 - }, { type: 'image', url: `https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0` + encodeURI(`&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}`) }, { type: 'image', - url: spec.SYNC_ENDPOINT3 + url: spec.SYNC_ENDPOINT2 }]; } } diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 1007305b324..b3d559d956f 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -641,6 +641,9 @@ const OPEN_RTB_PROTOCOL = { } utils.deepSetValue(request, 'regs.ext.gdpr', gdprApplies); utils.deepSetValue(request, 'user.ext.consent', firstBidRequest.gdprConsent.consentString); + if (firstBidRequest.gdprConsent.addtlConsent && typeof firstBidRequest.gdprConsent.addtlConsent === 'string') { + utils.deepSetValue(request, 'user.ext.ConsentedProvidersSettings.consented_providers', firstBidRequest.gdprConsent.addtlConsent); + } } // US Privacy (CCPA) support diff --git a/modules/priceFloors.js b/modules/priceFloors.js index eb1f3aed84c..2acd21918ef 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -138,10 +138,10 @@ function generatePossibleEnumerations(arrayOfFields, delimiter) { /** * @summary If a the input bidder has a registered cpmadjustment it returns the input CPM after being adjusted */ -export function getBiddersCpmAdjustment(bidderName, inputCpm) { +export function getBiddersCpmAdjustment(bidderName, inputCpm, bid = {}) { const adjustmentFunction = utils.deepAccess(getGlobal(), `bidderSettings.${bidderName}.bidCpmAdjustment`); if (adjustmentFunction) { - return parseFloat(adjustmentFunction(inputCpm)); + return parseFloat(adjustmentFunction(inputCpm, {...bid, cpm: inputCpm})); } return parseFloat(inputCpm); } @@ -682,7 +682,7 @@ export function addBidResponseHook(fn, adUnitCode, bid) { } // ok we got the bid response cpm in our desired currency. Now we need to run the bidders CPMAdjustment function if it exists - adjustedCpm = getBiddersCpmAdjustment(bid.bidderCode, adjustedCpm); + adjustedCpm = getBiddersCpmAdjustment(bid.bidderCode, adjustedCpm, bid); // add necessary data information for analytics adapters / floor providers would possibly need addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm); diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 2e52fd27cf1..d21854a57c4 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -2,6 +2,7 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; import {config} from '../src/config.js'; +import { Renderer } from '../src/Renderer.js'; const BIDDER_CODE = 'pubmatic'; const LOG_WARN_PREFIX = 'PubMatic: '; @@ -14,6 +15,8 @@ const UNDEFINED = undefined; const DEFAULT_WIDTH = 0; const DEFAULT_HEIGHT = 0; const PREBID_NATIVE_HELP_LINK = 'http://prebid.org/dev-docs/show-native-ads.html'; +const PUBLICATION = 'pubmatic'; // Your publication on Blue Billywig, potentially with environment (e.g. publication.bbvms.com or publication.test.bbvms.com) +const RENDERER_URL = 'https://pubmatic.bbvms.com/r/'.concat('$RENDERER', '.js'); // URL of the renderer application const CUSTOM_PARAMS = { 'kadpageurl': '', // Custom page url 'gender': '', // User gender @@ -104,6 +107,60 @@ const dealChannelValues = { 5: 'PREF', 6: 'PMPG' }; +// BB stands for Blue BillyWig +const BB_RENDERER = { + bootstrapPlayer: function(bid) { + const config = { + code: bid.adUnitCode, + }; + + if (bid.vastXml) config.vastXml = bid.vastXml; + else if (bid.vastUrl) config.vastUrl = bid.vastUrl; + + if (!bid.vastXml && !bid.vastUrl) { + utils.logWarn(`${LOG_WARN_PREFIX}: No vastXml or vastUrl on bid, bailing...`); + return; + } + + const rendererId = BB_RENDERER.getRendererId(PUBLICATION, bid.rendererCode); + + const ele = document.getElementById(bid.adUnitCode); // NB convention + + let renderer; + + for (let rendererIndex = 0; rendererIndex < window.bluebillywig.renderers.length; rendererIndex++) { + if (window.bluebillywig.renderers[rendererIndex]._id === rendererId) { + renderer = window.bluebillywig.renderers[rendererIndex]; + break; + } + } + + if (renderer) renderer.bootstrap(config, ele); + else utils.logWarn(`${LOG_WARN_PREFIX}: Couldn't find a renderer with ${rendererId}`); + }, + newRenderer: function(rendererCode, adUnitCode) { + var rendererUrl = RENDERER_URL.replace('$RENDERER', rendererCode); + const renderer = Renderer.install({ + url: rendererUrl, + loaded: false, + adUnitCode + }); + + try { + renderer.setRender(BB_RENDERER.outstreamRender); + } catch (err) { + utils.logWarn(`${LOG_WARN_PREFIX}: Error tying to setRender on renderer`, err); + } + + return renderer; + }, + outstreamRender: function(bid) { + bid.renderer.push(function() { BB_RENDERER.bootstrapPlayer(bid) }); + }, + getRendererId: function(pub, renderer) { + return `${pub}-${renderer}`; // NB convention! + } +}; let publisherId = 0; let isInvalidNativeRequest = false; @@ -760,6 +817,23 @@ function _handleDealCustomTargetings(payload, dctrArr, validBidRequests) { } } +function _assignRenderer(newBid, request) { + let bidParams, context, adUnitCode; + if (request.bidderRequest && request.bidderRequest.bids) { + for (let bidderRequestBidsIndex = 0; bidderRequestBidsIndex < request.bidderRequest.bids.length; bidderRequestBidsIndex++) { + if (request.bidderRequest.bids[bidderRequestBidsIndex].bidId === newBid.requestId) { + bidParams = request.bidderRequest.bids[bidderRequestBidsIndex].params; + context = request.bidderRequest.bids[bidderRequestBidsIndex].mediaTypes[VIDEO].context; + adUnitCode = request.bidderRequest.bids[bidderRequestBidsIndex].adUnitCode; + } + } + if (context && context === 'outstream' && bidParams && bidParams.outstreamAU && adUnitCode) { + newBid.rendererCode = bidParams.outstreamAU; + newBid.renderer = BB_RENDERER.newRenderer(newBid.rendererCode, adUnitCode); + } + } +}; + export const spec = { code: BIDDER_CODE, gvlid: 76, @@ -782,6 +856,19 @@ export const spec = { utils.logWarn(LOG_WARN_PREFIX + 'Error: For video ads, mimes is mandatory and must specify atlease 1 mime value. Call to OpenBid will not be sent for ad unit:' + JSON.stringify(bid)); return false; } + if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { + if (!bid.mediaTypes[VIDEO].hasOwnProperty('context')) { + utils.logError(`${LOG_WARN_PREFIX}: no context specified in bid. Rejecting bid: `, bid); + return false; + } + if (bid.mediaTypes[VIDEO].context === 'outstream' && !utils.isStr(bid.params.outstreamAU)) { + utils.logError(`${LOG_WARN_PREFIX}: for "outstream" bids outstreamAU is required. Rejecting bid: `, bid); + return false; + } + } else { + utils.logError(`${LOG_WARN_PREFIX}: mediaTypes or mediaTypes.video is not specified. Rejecting bid: `, bid); + return false; + } } return true; } @@ -865,6 +952,11 @@ export const spec = { payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim(); payload.site.domain = _getDomainFromURL(payload.site.page); + // add the content object from config in request + if (typeof config.getConfig('content') === 'object') { + payload.site.content = config.getConfig('content'); + } + // merge the device from config.getConfig('device') if (typeof config.getConfig('device') === 'object') { payload.device = Object.assign(payload.device, config.getConfig('device')); @@ -910,13 +1002,19 @@ export const spec = { // not copying domain from site as it is a derived value from page payload.app.publisher = payload.site.publisher; payload.app.ext = payload.site.ext || UNDEFINED; + // We will also need to pass content object in app.content if app object is also set into the config; + // BUT do not use content object from config if content object is present in app as app.content + if (typeof payload.app.content !== 'object') { + payload.app.content = payload.site.content || UNDEFINED; + } delete payload.site; } return { method: 'POST', url: ENDPOINT, - data: JSON.stringify(payload) + data: JSON.stringify(payload), + bidderRequest: bidderRequest }; }, @@ -966,6 +1064,7 @@ export const spec = { newBid.width = bid.hasOwnProperty('w') ? bid.w : req.video.w; newBid.height = bid.hasOwnProperty('h') ? bid.h : req.video.h; newBid.vastXml = bid.adm; + _assignRenderer(newBid, request); break; case NATIVE: _parseNativeResponse(bid, newBid); diff --git a/modules/pubmaticBidAdapter.md b/modules/pubmaticBidAdapter.md index a045bed3e2b..cd9398477f4 100644 --- a/modules/pubmaticBidAdapter.md +++ b/modules/pubmaticBidAdapter.md @@ -25,6 +25,7 @@ var adUnits = [ bidder: 'pubmatic', params: { publisherId: '156209', // required + oustreamAU: 'renderer_test_pubmatic', // required if mediaTypes-> video-> context is 'outstream'. This value can be get by BlueBillyWig Team. adSlot: 'pubmatic_test2', // optional pmzoneid: 'zone1, zone11', // optional lat: '40.712775', // optional diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index 91022d70df9..894bb991a71 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -85,11 +85,6 @@ function checkTCFv1(vendorData) { } function checkTCFv2(tcData) { - if (tcData.purposeOneTreatment && tcData.publisherCC === 'DE') { - // special purpose 1 treatment for Germany - return true; - } - let restrictions = tcData.publisher ? tcData.publisher.restrictions : {}; let qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT] ? restrictions[PURPOSE_DATA_COLLECT][QUANTCAST_VENDOR_ID] diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 3aa7753d204..9acd484cec8 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -23,14 +23,56 @@ */ /** - * @interface ModuleConfig + * @property + * @summary used to link submodule with config + * @name RtdSubmodule#config + * @type {Object} */ /** - * @property - * @summary sub module name - * @name ModuleConfig#name - * @type {string} + * @function + * @summary init sub module + * @name RtdSubmodule#init + * @param {Object} config + * @param {Object} gdpr settings + * @param {Object} usp settings + * @return {boolean} false to remove sub module + */ + +/** + * @function? + * @summary on auction init event + * @name RtdSubmodule#auctionInit + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @function? + * @summary on auction end event + * @name RtdSubmodule#auctionEnd + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @function? + * @summary on bid request event + * @name RtdSubmodule#updateBidRequest + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @function? + * @summary on bid response event + * @name RtdSubmodule#updateBidResponse + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @interface ModuleConfig */ /** @@ -40,18 +82,43 @@ * @type {number} */ +/** + * @property + * @summary timeout (if no auction dealy) + * @name ModuleConfig#timeout + * @type {number} + */ + +/** + * @property + * @summary list of sub modules + * @name ModuleConfig#dataProviders + * @type {SubmoduleConfig[]} + */ + +/** + * @interface SubModuleConfig + */ + /** * @property * @summary params for provide (sub module) - * @name ModuleConfig#params + * @name SubModuleConfig#params * @type {Object} */ /** * @property - * @summary timeout (if no auction dealy) - * @name ModuleConfig#timeout - * @type {number} + * @summary name + * @name ModuleConfig#name + * @type {string} + */ + +/** + * @property + * @summary delay auction for this sub module + * @name ModuleConfig#waitForIt + * @type {boolean} */ import {getGlobal} from '../../src/prebidGlobal.js'; @@ -59,15 +126,21 @@ import {config} from '../../src/config.js'; import {targeting} from '../../src/targeting.js'; import {getHook, module} from '../../src/hook.js'; import * as utils from '../../src/utils.js'; +import events from '../../src/events.js'; +import CONSTANTS from '../../src/constants.json'; +import {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; +import find from 'core-js-pure/features/array/find.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; /** @type {number} */ const DEF_TIMEOUT = 1000; /** @type {RtdSubmodule[]} */ -let subModules = []; +export let subModules = []; /** @type {ModuleConfig} */ let _moduleConfig; +/** @type {SubmoduleConfig[]} */ +let _dataProviders = []; /** * enable submodule in User ID @@ -85,6 +158,9 @@ export function init(config) { } confListener(); // unsubscribe config listener _moduleConfig = realTimeData; + _dataProviders = realTimeData.dataProviders; + getHook('makeBidRequests').before(initSubModules); + setEventsListeners(); if (typeof (_moduleConfig.auctionDelay) === 'undefined') { _moduleConfig.auctionDelay = 0; } @@ -97,61 +173,82 @@ export function init(config) { }); } +/** + * call each sub module init function by config order + * if no init function / init return failure / module not configured - remove it from submodules list + */ +export function initSubModules(next, adUnits, auctionStart, auctionId, cbTimeout, labels) { + let subModulesByOrder = []; + _dataProviders.forEach(provider => { + const sm = find(subModules, s => s.name === provider.name); + const initResponse = sm && sm.init && sm.init(provider, gdprDataHandler.getConsentData(), uspDataHandler.getConsentData()); + if (initResponse) { + subModulesByOrder.push(Object.assign(sm, {config: provider})); + } + }); + subModules = subModulesByOrder; + next(adUnits, auctionStart, auctionId, cbTimeout, labels) +} + +/** + * call each sub module event function by config order + */ +function setEventsListeners() { + events.on(CONSTANTS.EVENTS.AUCTION_INIT, (args) => { + subModules.forEach(sm => { sm.auctionInit && sm.auctionInit(args, sm.config) }) + }); + events.on(CONSTANTS.EVENTS.AUCTION_END, (args) => { + subModules.forEach(sm => { sm.auctionEnd && sm.auctionEnd(args, sm.config) }) + }); + events.on(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, (args) => { + subModules.forEach(sm => { sm.updateBidRequest && sm.updateBidRequest(args, sm.config) }) + }); + events.on(CONSTANTS.EVENTS.BID_RESPONSE, (args) => { + subModules.forEach(sm => { sm.updateBidResponse && sm.updateBidResponse(args, sm.config) }) + }); +} + /** * get data from sub module * @param {AdUnit[]} adUnits received from auction * @param {function} callback callback function on data received */ -function getProviderData(adUnits, callback) { - const callbackExpected = subModules.length; - let dataReceived = []; - let processDone = false; - const dataWaitTimeout = setTimeout(() => { - processDone = true; - callback(dataReceived); - }, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); +export function getProviderData(adUnits, callback) { + /** + * invoke callback if one of the conditions met: + * timeout reached + * all submodules answered + * all sub modules configured "waitForIt:true" answered (as long as there is at least one configured) + */ + const waitForSubModulesLength = subModules.filter(sm => sm.config && sm.config.waitForIt).length; + let callbacksExpected = waitForSubModulesLength || subModules.length; + const shouldWaitForAllSubModules = waitForSubModulesLength === 0; + let dataReceived = {}; + let processDone = false; + const dataWaitTimeout = setTimeout(done, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); subModules.forEach(sm => { - sm.getData(adUnits, onDataReceived); + sm.getData(adUnits, onDataReceived.bind(sm)); }); function onDataReceived(data) { if (processDone) { return } - dataReceived.push(data); - if (dataReceived.length === callbackExpected) { - processDone = true; + dataReceived[this.name] = data; + if (shouldWaitForAllSubModules || (this.config && this.config.waitForIt)) { + callbacksExpected-- + } + if (callbacksExpected <= 0) { clearTimeout(dataWaitTimeout); - callback(dataReceived); + done(); } } -} -/** - * delete invalid data received from provider - * this is to ensure working flow for GPT - * @param {Object} data received from provider - * @return {Object} valid data for GPT targeting - */ -export function validateProviderDataForGPT(data) { - // data must be an object, contains object with string as value - if (typeof data !== 'object') { - return {}; - } - for (let key in data) { - if (data.hasOwnProperty(key)) { - for (let innerKey in data[key]) { - if (data[key].hasOwnProperty(innerKey)) { - if (typeof data[key][innerKey] !== 'string') { - utils.logWarn(`removing ${key}: {${innerKey}:${data[key][innerKey]} } from GPT targeting because of invalid type (must be string)`); - delete data[key][innerKey]; - } - } - } - } + function done() { + processDone = true; + callback(dataReceived); } - return data; } /** @@ -163,7 +260,7 @@ export function validateProviderDataForGPT(data) { export function setTargetsAfterRequestBids(next, adUnits) { getProviderData(adUnits, (data) => { if (data && Object.keys(data).length) { - const _mergedData = deepMerge(data); + const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); if (Object.keys(_mergedData).length) { setDataForPrimaryAdServer(_mergedData); } @@ -172,6 +269,22 @@ export function setTargetsAfterRequestBids(next, adUnits) { }); } +/** + * return an array providers data in reverse order,so the data merge will be according to correct config order + * @param {Submodule[]} modules + * @param {Object} data - data retrieved from providers + * @return {array} reversed order ready for merge + */ +function setDataOrderByProvider(modules, data) { + let rd = []; + for (let i = modules.length; i--; i > 0) { + if (data[modules[i].name]) { + rd.push(data[modules[i].name]) + } + } + return rd; +} + /** * deep merge array of objects * @param {array} arr - objects array @@ -207,7 +320,7 @@ export function deepMerge(arr) { export function requestBidsHook(fn, reqBidsConfigObj) { getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits, (data) => { if (data && Object.keys(data).length) { - const _mergedData = deepMerge(data); + const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); if (Object.keys(_mergedData).length) { setDataForPrimaryAdServer(_mergedData); addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, _mergedData); @@ -222,7 +335,6 @@ export function requestBidsHook(fn, reqBidsConfigObj) { * @param {Object} data - key values to set */ function setDataForPrimaryAdServer(data) { - data = validateProviderDataForGPT(data); if (utils.isGptPubadsDefined()) { targeting.setTargetingForGPT(data, null) } else { @@ -247,5 +359,5 @@ function addIdDataToAdUnitBids(adUnits, data) { }); } -init(config); module('realTimeData', attachRealTimeDataProvider); +init(config); diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 7a1bdce84d5..0bee4d926fc 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -40,8 +40,6 @@ const cache = { timeouts: {}, }; -const validFloorProviders = ['rubicon', 'rubi', 'magnite', 'mgni']; - export function getHostNameFromReferer(referer) { try { rubiconAdapter.referrerHostname = utils.parseUrl(referer, {noDecodeWholeURL: true}).hostname; @@ -200,17 +198,19 @@ function sendMessage(auctionId, bidWonId) { if (auctionCache.floorData.location === 'noData') { auction.floors = utils.pick(auctionCache.floorData, [ 'location', - 'fetchStatus' + 'fetchStatus', + 'floorProvider as provider' ]); } else { auction.floors = utils.pick(auctionCache.floorData, [ 'location', - 'modelName', () => auctionCache.floorData.modelVersion, + 'modelVersion as modelName', 'skipped', 'enforcement', () => utils.deepAccess(auctionCache.floorData, 'enforcements.enforceJS'), 'dealsEnforced', () => utils.deepAccess(auctionCache.floorData, 'enforcements.floorDeals'), 'skipRate', - 'fetchStatus' + 'fetchStatus', + 'floorProvider as provider' ]); } } @@ -285,7 +285,7 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { if (previousBidResponse && previousBidResponse.bidPriceUSD > responsePrice) { return previousBidResponse; } - let bidResponse = utils.pick(bid, [ + return utils.pick(bid, [ 'bidPriceUSD', () => responsePrice, 'dealId', 'status', @@ -295,12 +295,9 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { 'height' ]), 'seatBidId', + 'floorValue', () => utils.deepAccess(bid, 'floorData.floorValue'), + 'floorRule', () => utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined ]); - if (auctionFloorData) { - bidResponse.floorValue = utils.deepAccess(bid, 'floorData.floorValue'); - bidResponse.floorRule = utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined - } - return bidResponse; } let samplingFactor = 1; @@ -380,9 +377,9 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.bids = {}; cacheEntry.bidsWon = {}; cacheEntry.referrer = args.bidderRequests[0].refererInfo.referer; - const floorProvider = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData.floorProvider'); - if (validFloorProviders.indexOf(floorProvider) !== -1) { - cacheEntry.floorData = {...args.bidderRequests[0].bids[0].floorData}; + const floorData = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData'); + if (floorData) { + cacheEntry.floorData = {...floorData}; } cache.auctions[args.auctionId] = cacheEntry; break; @@ -468,7 +465,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { bid.adUnit.adSlot = args.floorData.matchedFields.gptSlot; } // if we have not set enforcements yet set it - if (auctionEntry.floorData && !auctionEntry.floorData.enforcements && utils.deepAccess(args, 'floorData.enforcements')) { + if (!utils.deepAccess(auctionEntry, 'floorData.enforcements') && utils.deepAccess(args, 'floorData.enforcements')) { auctionEntry.floorData.enforcements = {...args.floorData.enforcements}; } if (!bid) { @@ -492,7 +489,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { }; } bid.clientLatencyMillis = Date.now() - cache.auctions[args.auctionId].timestamp; - bid.bidResponse = parseBidResponse(args, bid.bidResponse, auctionEntry.floorData); + bid.bidResponse = parseBidResponse(args, bid.bidResponse); break; case BIDDER_DONE: args.bids.forEach(bid => { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 0c563987331..4a0dd10281b 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -2,6 +2,7 @@ import * as utils 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 find from 'core-js-pure/features/array/find.js'; const DEFAULT_INTEGRATION = 'pbjs_lite'; const DEFAULT_PBS_INTEGRATION = 'pbjs'; @@ -203,11 +204,16 @@ export const spec = { let bidFloor; if (typeof bidRequest.getFloor === 'function' && !config.getConfig('rubicon.disableFloors')) { - let floorInfo = bidRequest.getFloor({ - currency: 'USD', - mediaType: 'video', - size: parseSizes(bidRequest, 'video') - }); + let floorInfo; + try { + floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'video', + size: parseSizes(bidRequest, 'video') + }); + } catch (e) { + utils.logError('Rubicon: getFloor threw an error: ', e); + } bidFloor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; } else { bidFloor = parseFloat(utils.deepAccess(bidRequest, 'params.floor')); @@ -242,59 +248,25 @@ export const spec = { utils.deepSetValue(data, 'regs.ext.us_privacy', bidderRequest.uspConsent); } - if (bidRequest.userId && typeof bidRequest.userId === 'object' && - (bidRequest.userId.tdid || bidRequest.userId.pubcid || bidRequest.userId.lipb || bidRequest.userId.idl_env)) { - utils.deepSetValue(data, 'user.ext.eids', []); - - if (bidRequest.userId.tdid) { - data.user.ext.eids.push({ - source: 'adserver.org', - uids: [{ - id: bidRequest.userId.tdid, - ext: { - rtiPartner: 'TDID' - } - }] - }); - } - - if (bidRequest.userId.pubcid) { - data.user.ext.eids.push({ - source: 'pubcommon', - uids: [{ - id: bidRequest.userId.pubcid, - }] - }); - } - - // support liveintent ID - if (bidRequest.userId.lipb && bidRequest.userId.lipb.lipbid) { - data.user.ext.eids.push({ - source: 'liveintent.com', - uids: [{ - id: bidRequest.userId.lipb.lipbid - }] - }); - - data.user.ext.tpid = { - source: 'liveintent.com', - uid: bidRequest.userId.lipb.lipbid - }; - - if (Array.isArray(bidRequest.userId.lipb.segments) && bidRequest.userId.lipb.segments.length) { - utils.deepSetValue(data, 'rp.target.LIseg', bidRequest.userId.lipb.segments); + const eids = utils.deepAccess(bidderRequest, 'bids.0.userIdAsEids'); + if (eids && eids.length) { + // filter out unsupported id systems + utils.deepSetValue(data, 'user.ext.eids', eids.filter(eid => ['adserver.org', 'pubcid.org', 'liveintent.com', 'liveramp.com', 'sharedid.org'].indexOf(eid.source) !== -1)); + + // liveintent requires additional props to be set + const liveIntentEid = find(data.user.ext.eids, eid => eid.source === 'liveintent.com'); + if (liveIntentEid) { + utils.deepSetValue(data, 'user.ext.tpid', { source: liveIntentEid.source, uid: liveIntentEid.uids[0].id }); + if (liveIntentEid.ext && liveIntentEid.ext.segments) { + utils.deepSetValue(data, 'rp.target.LIseg', liveIntentEid.ext.segments); } } + } - // support identityLink (aka LiveRamp) - if (bidRequest.userId.idl_env) { - data.user.ext.eids.push({ - source: 'liveramp.com', - uids: [{ - id: bidRequest.userId.idl_env - }] - }); - } + // set user.id value from config value + const configUserId = config.getConfig('user.id'); + if (configUserId) { + utils.deepSetValue(data, 'user.id', configUserId); } if (config.getConfig('coppa') === true) { @@ -527,11 +499,16 @@ export const spec = { // If floors module is enabled and we get USD floor back, send it in rp_hard_floor else undfined if (typeof bidRequest.getFloor === 'function' && !config.getConfig('rubicon.disableFloors')) { - let floorInfo = bidRequest.getFloor({ - currency: 'USD', - mediaType: 'banner', - size: '*' - }); + let floorInfo; + try { + floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'banner', + size: '*' + }); + } catch (e) { + utils.logError('Rubicon: getFloor threw an error: ', e); + } data['rp_hard_floor'] = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; } @@ -554,10 +531,21 @@ export const spec = { // support identityLink (aka LiveRamp) if (bidRequest.userId.idl_env) { - data['tpid_liveramp.com'] = bidRequest.userId.idl_env; + data['x_liverampidl'] = bidRequest.userId.idl_env; + } + + // support shared id + if (bidRequest.userId.sharedid) { + data['eid_sharedid.org'] = `${bidRequest.userId.sharedid.id}^3^${bidRequest.userId.sharedid.third}`; } } + // set ppuid value from config value + const configUserId = config.getConfig('user.id'); + if (configUserId) { + data['ppuid'] = configUserId; + } + if (bidderRequest.gdprConsent) { // add 'gdpr' only if 'gdprApplies' is defined if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { @@ -1166,7 +1154,7 @@ export function hasValidSupplyChainParams(schain) { if (!schain.nodes) return isValid; isValid = schain.nodes.reduce((status, node) => { if (!status) return status; - return requiredFields.every(field => node[field]); + return requiredFields.every(field => node.hasOwnProperty(field)); }, true); if (!isValid) utils.logError('Rubicon: required schain params missing'); return isValid; diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js new file mode 100644 index 00000000000..a745c54e39c --- /dev/null +++ b/modules/smartxBidAdapter.js @@ -0,0 +1,400 @@ +import * as utils from '../src/utils.js'; +import { + Renderer +} from '../src/Renderer.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { + VIDEO +} from '../src/mediaTypes.js'; +const BIDDER_CODE = 'smartx'; +const URL = 'https://bid.sxp.smartclip.net/bid/1000'; +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO], + /** + * Determines whether or not the given bid request is valid. + * From Prebid.js: isBidRequestValid - Verify the the AdUnits.bids, respond with true (valid) or false (invalid). + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + if (bid && typeof bid.params !== 'object') { + utils.logError(BIDDER_CODE + ': params is not defined or is incorrect in the bidder settings.'); + return false; + } + if (!utils.deepAccess(bid, 'mediaTypes.video')) { + utils.logError(BIDDER_CODE + ': mediaTypes.video is not present in the bidder settings.'); + return false; + } + const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); + if (!playerSize || !utils.isArray(playerSize)) { + utils.logError(BIDDER_CODE + ': mediaTypes.video.playerSize is not defined in the bidder settings.'); + return false; + } + if (!utils.getBidIdParameter('tagId', bid.params)) { + utils.logError(BIDDER_CODE + ': tagId is not present in bidder params'); + return false; + } + if (!utils.getBidIdParameter('publisherId', bid.params)) { + utils.logError(BIDDER_CODE + ': publisherId is not present in bidder params'); + return false; + } + if (!utils.getBidIdParameter('siteId', bid.params)) { + utils.logError(BIDDER_CODE + ': siteId is not present in bidder params'); + return false; + } + if (!utils.getBidIdParameter('bidfloor', bid.params)) { + utils.logError(BIDDER_CODE + ': bidfloor is not present in bidder params'); + return false; + } + if (!utils.getBidIdParameter('bidfloorcur', bid.params)) { + utils.logError(BIDDER_CODE + ': bidfloorcur is not present in bidder params'); + return false; + } + if (utils.deepAccess(bid, 'mediaTypes.video.context') === 'outstream') { + if (!utils.getBidIdParameter('outstream_options', bid.params)) { + utils.logError(BIDDER_CODE + ': outstream_options parameter is not defined'); + return false; + } + if (!utils.getBidIdParameter('slot', bid.params.outstream_options)) { + utils.logError(BIDDER_CODE + ': slot parameter is not defined in outstream_options object in the configuration'); + return false; + } + if (!utils.getBidIdParameter('outstream_function', bid.params)) { + utils.logMessage(BIDDER_CODE + ': outstream_function parameter is not defined. The default outstream renderer will be injected in the header. You can override the default SmartX outstream rendering by defining your own Outstream function using field outstream_function.'); + return true; + } + } + + return true; + }, + /** + * Make a server request from the list of BidRequests. + * from Prebid.js: buildRequests - Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (bidRequests, bidderRequest) { + const page = bidderRequest.refererInfo.referer; + const isPageSecure = !!page.match(/^https:/) + + const smartxRequests = bidRequests.map(function (bid) { + const tagId = utils.getBidIdParameter('tagId', bid.params); + const publisherId = utils.getBidIdParameter('publisherId', bid.params); + const bidfloor = utils.getBidIdParameter('bidfloor', bid.params); + const bidfloorcur = utils.getBidIdParameter('bidfloorcur', bid.params); + const siteId = utils.getBidIdParameter('siteId', bid.params); + const domain = utils.getBidIdParameter('domain', bid.params); + const cat = utils.getBidIdParameter('cat', bid.params); + let pubcid = null; + const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); + const contentWidth = playerSize[0][0]; + const contentHeight = playerSize[0][1]; + const secure = +(isPageSecure || (utils.getBidIdParameter('secure', bid.params) ? 1 : 0)); + const ext = { + sdk_name: 'Prebid 1+' + }; + const mimes = utils.getBidIdParameter('mimes', bid.params) || ['application/javascript', 'video/mp4', 'video/webm']; + const linearity = utils.getBidIdParameter('linearity', bid.params) || 1; + const minduration = utils.getBidIdParameter('minduration', bid.params) || 0; + const maxduration = utils.getBidIdParameter('maxduration', bid.params) || 500; + const startdelay = utils.getBidIdParameter('startdelay', bid.params) || 0; + const minbitrate = utils.getBidIdParameter('minbitrate', bid.params) || 0; + const maxbitrate = utils.getBidIdParameter('maxbitrate', bid.params) || 3500; + const delivery = utils.getBidIdParameter('delivery', bid.params) || [2]; + const pos = utils.getBidIdParameter('pos', bid.params) || 1; + const api = utils.getBidIdParameter('api', bid.params) || [2]; + const protocols = utils.getBidIdParameter('protocols', bid.params) || [2, 3, 5, 6]; + var contextcustom = utils.deepAccess(bid, 'mediaTypes.video.context'); + var placement = 1; + + if (contextcustom === 'outstream') { + placement = 3; + } + + let smartxReq = { + id: bid.bidId, + secure: secure, + bidfloor: bidfloor, + bidfloorcur: bidfloorcur, + video: { + w: contentWidth, + h: contentHeight, + mimes: mimes, + linearity: linearity, + minduration: minduration, + maxduration: maxduration, + startdelay: startdelay, + protocols: protocols, + minbitrate: minbitrate, + maxbitrate: maxbitrate, + delivery: delivery, + pos: pos, + placement: placement, + api: api, + ext: ext + }, + tagid: tagId, + ext: { + 'smart.bidpricetype': 1 + } + }; + + if (bid.crumbs && bid.crumbs.pubcid) { + pubcid = bid.crumbs.pubcid; + } + + const language = navigator.language ? 'language' : 'userLanguage'; + const device = { + h: screen.height, + w: screen.width, + dnt: utils.getDNT() ? 1 : 0, + language: navigator[language].split('-')[0], + make: navigator.vendor ? navigator.vendor : '', + ua: navigator.userAgent + }; + const at = utils.getBidIdParameter('at', bid.params) || 2; + const cur = utils.getBidIdParameter('cur', bid.params) || ['EUR']; + const requestPayload = { + id: utils.generateUUID(), + imp: smartxReq, + site: { + id: siteId, + page: page, + cat: cat, + content: 'content', + domain: domain, + publisher: { + id: publisherId + } + }, + device: device, + at: at, + cur: cur + }; + const userExt = {}; + + // Add GDPR flag and consent string + if (bidderRequest && bidderRequest.gdprConsent) { + userExt.consent = bidderRequest.gdprConsent.consentString; + if (typeof bidderRequest.gdprConsent.gdprApplies !== 'undefined') { + requestPayload.regs = { + ext: { + gdpr: (bidderRequest.gdprConsent.gdprApplies ? 1 : 0) + } + }; + } + } + + // Add common id if available + if (pubcid) { + userExt.fpc = pubcid; + } + + // Only add the user object if it's not empty + if (!utils.isEmpty(userExt)) { + requestPayload.user = { + ext: userExt + }; + } + + // Targeting + if (utils.getBidIdParameter('data', bid.params.user)) { + var targetingarr = []; + for (var i = 0; i < bid.params.user.data.length; i++) { + var isemq = (bid.params.user.data[i].name) || 'empty'; + if (isemq !== 'empty') { + var provider = bid.params.user.data[i].name; + var targetingstring = (bid.params.user.data[i].segment[0].value) || 'empty'; + targetingarr.push({ + id: provider, + name: provider, + segment: { + name: provider, + value: targetingstring, + } + }) + } + } + + requestPayload.user = { + ext: userExt, + data: targetingarr + } + } + + return { + method: 'POST', + url: URL, + data: requestPayload, + bidRequest: bidderRequest, + options: { + contentType: 'application/json', + customHeaders: { + 'x-openrtb-version': '2.3' + } + } + }; + }); + + return smartxRequests; + }, + /** + * 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, bidderRequest) { + const bidResponses = []; + const serverResponseBody = serverResponse.body; + if (serverResponseBody && utils.isArray(serverResponseBody.seatbid)) { + utils._each(serverResponseBody.seatbid, function (bids) { + utils._each(bids.bid, function (smartxBid) { + let currentBidRequest = {}; + for (let i in bidderRequest.bidRequest.bids) { + if (smartxBid.impid == bidderRequest.bidRequest.bids[i].bidId) { + currentBidRequest = bidderRequest.bidRequest.bids[i]; + } + } + /** + * Make sure currency and price are the right ones + * TODO: what about the pre_market_bid partners sizes? + */ + utils._each(currentBidRequest.params.pre_market_bids, function (pmb) { + if (pmb.deal_id == smartxBid.id) { + smartxBid.price = pmb.price; + serverResponseBody.cur = pmb.currency; + } + }); + const bid = { + requestId: currentBidRequest.bidId, + currency: serverResponseBody.cur || 'USD', + cpm: smartxBid.price, + creativeId: smartxBid.crid || '', + ttl: 360, + netRevenue: true, + vastContent: smartxBid.adm, + vastXml: smartxBid.adm, + mediaType: VIDEO, + width: smartxBid.w, + height: smartxBid.h + }; + const context = utils.deepAccess(currentBidRequest, 'mediaTypes.video.context'); + if (context === 'outstream') { + const playersize = utils.deepAccess(currentBidRequest, 'mediaTypes.video.playerSize'); + const renderer = Renderer.install({ + id: 0, + url: '//', + config: { + adText: 'SmartX Outstream Video Ad via Prebid.js', + player_width: playersize[0][0], + player_height: playersize[0][1], + content_page_url: utils.deepAccess(bidderRequest, 'data.site.page'), + ad_mute: +!!utils.deepAccess(currentBidRequest, 'params.ad_mute'), + hide_skin: +!!utils.deepAccess(currentBidRequest, 'params.hide_skin'), + outstream_options: utils.deepAccess(currentBidRequest, 'params.outstream_options'), + outstream_function: utils.deepAccess(currentBidRequest, 'params.outstream_function') + } + }); + try { + renderer.setRender(outstreamRender); + renderer.setEventHandlers({ + impression: function impression() { + return utils.logMessage('SmartX outstream video impression event'); + }, + loaded: function loaded() { + return utils.logMessage('SmartX outstream video loaded event'); + }, + ended: function ended() { + utils.logMessage('SmartX outstream renderer video event'); + } + }); + } catch (err) { + utils.logWarn('Prebid Error calling setRender or setEventHandlers on renderer', err); + } + bid.renderer = renderer; + } + bidResponses.push(bid); + }) + }); + } + return bidResponses; + } +} + +function createOutstreamScript(bid) { + // const slot = utils.getBidIdParameter('slot', bid.renderer.config.outstream_options); + utils.logMessage('[SMARTX][renderer] Handle SmartX outstream renderer'); + const elementId = bid.adUnitCode; + // eslint-disable-next-line camelcase + var sc_smartIntxtStart; + // eslint-disable-next-line camelcase + var sc_smartIntxtNoad; + // eslint-disable-next-line camelcase + var sc_smartIntxtEnd; + var SmartPlay; + let smartPlayObj = { + minAdWidth: 290, + maxAdWidth: 900, + elementLocator: { + allowInViewport: false, + minimumElementWidth: 290, + scanPixelsBelowViewport: 800 + }, + onStartCallback: function (m, n) { + try { + sc_smartIntxtStart(n); + } catch (f) {} + }, + onCappedCallback: function (m, n) { + try { + sc_smartIntxtNoad(n); + } catch (f) {} + }, + onEndCallback: function (m, n) { + try { + sc_smartIntxtEnd(n); + } catch (f) {} + }, + debug: true + }; + smartPlayObj.adResponse = bid.vastContent; + const script = window.document.createElement('script'); + script.type = 'text/javascript'; + script.async = 'true'; + script.src = 'https://dco.smartclip.net/?plc=7777777'; + script.onload = script.onreadystatechange = function () { + var rs = this.readyState; + if (rs && rs != 'complete' && rs != 'loaded') return; + try { + SmartPlay(elementId, smartPlayObj); + } catch (e) { + utils.logError('error caught : ' + e); + } + }; + return script; +} + +function outstreamRender(bid) { + const script = createOutstreamScript(bid); + if (bid.renderer.config.outstream_function != null && typeof bid.renderer.config.outstream_function === 'function') { + bid.renderer.config.outstream_function(bid, script); + } else { + try { + const slot = utils.getBidIdParameter('slot', bid.renderer.config.outstream_options); + if (slot && window.document.getElementById(slot)) { + window.document.getElementById(slot).appendChild(script); + } else { + window.document.getElementsByTagName('head')[0].appendChild(script); + } + } catch (err) { + utils.logError('[SMARTX][renderer] Error:' + err.message) + } + } +} +registerBidder(spec); diff --git a/modules/smartxBidAdapter.md b/modules/smartxBidAdapter.md new file mode 100644 index 00000000000..a53af839e2b --- /dev/null +++ b/modules/smartxBidAdapter.md @@ -0,0 +1,159 @@ +# Overview + +``` +Module Name: smartclip Bidder Adapter +Module Type: Bidder Adapter +Maintainer: adtech@smartclip.tv +``` + +# Description + +Connect to smartx for bids. + +This adapter requires setup and approval from the smartclip team. + +# Test Parameters - Use case #1 - Out-Stream example and default rendering options +``` + var adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 360] + } + }, + bids: [{ + bidder: 'smartx', + params: { + tagId: 'Nu68JuOWAvrbzoyrOR9a7A', + publisherId: '11986', + siteId: '22860', + bidfloor: 0.3, + bidfloorcur: "EUR", + at: 2, + cur: ["EUR"], + outstream_options: { + slot: 'video1' + }, + } + }], + }]; +``` + +# Test Parameters - Use case #2 - Out-Stream with targeting example and default rendering options +``` + var adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 360] + } + }, + bids: [{ + bidder: 'smartx', + params: { + tagId: 'Nu68JuOWAvrbzoyrOR9a7A', + publisherId: '11986', + siteId: '22860', + bidfloor: 0.3, + bidfloorcur: "EUR", + at: 2, + cur: ["EUR"], + outstream_options: { + slot: 'video1' + }, + user: { + data: [{ + id: 'emq', + name: 'emq', + segment: [{ + id: 'emq', + name: 'emq', + value: 'e0:k14:e24' + }] + }, { + id: 'gs', + name: 'gs', + segment: [{ + id: 'gs', + name: 'gs', + value: 'tone_of_voice_dislike:tone_of_voice_negative:gs_health' + }] + }] + } + } + }] + }]; +``` + +# Test Parameters - Use case #3 - In-Stream example and default rendering options +``` + var adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 360] + } + }, + bids: [{ + bidder: 'smartx', + params: { + tagId: 'Nu68JuOWAvrbzoyrOR9a7A', + publisherId: '11986', + siteId: '22860', + bidfloor: 0.3, + bidfloorcur: "EUR", + at: 2, + cur: ["EUR"] + } + }], + }]; +``` + +# Test Parameters - Use case #4 - In-Stream with targeting example and default rendering options +``` + var adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 360] + } + }, + bids: [{ + bidder: 'smartx', + params: { + tagId: 'Nu68JuOWAvrbzoyrOR9a7A', + publisherId: '11986', + siteId: '22860', + bidfloor: 0.3, + bidfloorcur: "EUR", + at: 2, + cur: ["EUR"], + user: { + data: [{ + id: 'emq', + name: 'emq', + segment: [{ + id: 'emq', + name: 'emq', + value: 'e0:k14:e24' + }] + }, + { + id: 'gs', + name: 'gs', + segment: [{ + id: 'gs', + name: 'gs', + value: 'tone_of_voice_dislike:tone_of_voice_negative:gs_health' + }] + } + ] + } + } + }], + }]; +``` \ No newline at end of file diff --git a/modules/sspBCAdapter.js b/modules/sspBCAdapter.js new file mode 100644 index 00000000000..ef89fb08449 --- /dev/null +++ b/modules/sspBCAdapter.js @@ -0,0 +1,319 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'sspBC'; +const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; +const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; +const TMAX = 450; +const BIDDER_VERSION = '4.5'; +const W = window; +const { navigator } = W; + +const cookieSupport = () => { + const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); + const useCookies = navigator.cookieEnabled || !!document.cookie.length; + + return !isSafari && useCookies; +}; + +const applyClientHints = ortbRequest => { + const connection = navigator.connection || false; + const viewport = W.visualViewport || false; + const segments = []; + const hints = { + 'CH-Ect': connection.effectiveType, + 'CH-Rtt': connection.rtt, + 'CH-SaveData': connection.saveData, + 'CH-Downlink': connection.downlink, + 'CH-DeviceMemory': navigator.deviceMemory, + 'CH-Dpr': W.devicePixelRatio, + 'CH-ViewportWidth': viewport.width, + }; + + Object.keys(hints).forEach(key => { + const hint = hints[key]; + + if (hint) { + segments.push({ + name: key, + value: hint.toString(), + }); + } + }); + const data = [ + { + id: '12', + name: 'NetInfo', + segment: segments, + }]; + + ortbRequest.user = Object.assign(ortbRequest.user, { data }); +}; + +function applyGdpr(bidderRequest, ortbRequest) { + if (bidderRequest && bidderRequest.gdprConsent) { + ortbRequest.regs = Object.assign(ortbRequest.regs, { '[ortb_extensions.gdpr]': bidderRequest.gdprConsent.gdprApplies ? 1 : 0 }); + ortbRequest.user = Object.assign(ortbRequest.user, { '[ortb_extensions.consent]': bidderRequest.gdprConsent.consentString }); + } +} + +function setOnAny(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = utils.deepAccess(collection[i], key); + + if (result) { + return result; + } + } +} + +/** + * @param {object} slot Ad Unit Params by Prebid + * @returns {object} Banner by OpenRTB 2.5 §3.2.6 + */ +function mapBanner(slot) { + if (slot.mediaType === 'banner' || + utils.deepAccess(slot, 'mediaTypes.banner') || + (!slot.mediaType && !slot.mediaTypes)) { + const format = slot.sizes.map(size => ({ + w: size[0], + h: size[1], + })); + + // override - tylko 1szy wymiar + // format = format.slice(0, 1); + return { + format, + id: slot.bidId, + }; + } +} + +function mapImpression(slot) { + const imp = { + id: slot.params.id, + banner: mapBanner(slot), + /* native: mapNative(slot), */ + tagid: slot.params.id, + }; + + const bidfloor = parseFloat(slot.params.bidfloor); + + if (bidfloor) { + imp.bidfloor = bidfloor; + } + + return imp; +} + +function renderCreative(site, auctionId, bid, seat, request) { + let gam; + + const mcad = { + id: auctionId, + seat, + seatbid: [{ + bid: [bid], + }], + }; + + const mcbase = btoa(encodeURI(JSON.stringify(mcad))); + + if (bid.adm) { + // parse adm for gam config + try { + gam = JSON.parse(bid.adm).gam; + + if (!gam || !Object.keys(gam).length) { + gam = undefined; + } else { + gam.namedSizes = ['fluid']; + gam.div = 'div-gpt-ad-x01'; + gam.targeting = Object.assign(gam.targeting || {}, { + OAS_retarg: '0', + PREBID_ON: '1', + emptygaf: '0', + }); + } + + if (gam && !gam.targeting) { + gam.targeting = {}; + } + } catch (err) { + utils.logWarn('Could not parse adm data', bid.adm); + } + } + + let adcode = ` + + + + + + + +
+ + + `; + + return adcode; +} + +const spec = { + code: BIDDER_CODE, + aliases: [], + supportedMediaTypes: [BANNER], + isBidRequestValid(bid) { + if (bid.params && bid.params.siteId && bid.params.id) { + return true; + } + + return false; + }, + buildRequests(validBidRequests, bidderRequest) { + if ((!validBidRequests) || (validBidRequests.length < 1)) { + return false; + } + + const siteId = setOnAny(validBidRequests, 'params.siteId'); + const page = setOnAny(validBidRequests, 'params.page') || bidderRequest.refererInfo.referer; + const domain = setOnAny(validBidRequests, 'params.domain') || utils.parseUrl(page).hostname; + const tmax = setOnAny(validBidRequests, 'params.tmax') ? parseInt(setOnAny(validBidRequests, 'params.tmax'), 10) : TMAX; + const pbver = '$prebid.version$'; + const testMode = setOnAny(validBidRequests, 'params.test') ? 1 : undefined; + + let ref; + + try { + if (W.self === W.top && document.referrer) { ref = document.referrer; } + } catch (e) { + } + + const payload = { + id: bidderRequest.auctionId, + site: { id: siteId, page, domain, ref }, + imp: validBidRequests.map(slot => mapImpression(slot)), + tmax, + user: {}, + regs: {}, + test: testMode, + }; + + applyGdpr(bidderRequest, payload); + applyClientHints(payload); + + return { + method: 'POST', + url: BIDDER_URL + '?cs=' + cookieSupport() + '&bdver=' + BIDDER_VERSION + '&pbver=' + pbver + '&inver=0', + data: JSON.stringify(payload), + bidderRequest, + }; + }, + + interpretResponse(serverResponse, request) { + const response = serverResponse.body; + const bids = []; + let site = JSON.parse(request.data).site; // get page and referer data from request + site.sn = response.sn || 'mc_adapter'; // WPM site name (wp_sn) + let seat; + + if (response.seatbid !== undefined) { + response.seatbid.forEach(seatbid => { + seat = seatbid.seat; + seatbid.bid.forEach(serverBid => { + const bidRequest = request.bidderRequest.bids.filter(b => b.params.id === serverBid.impid)[0]; + + if (bidRequest) { + const bidFloor = bidRequest.params.bidFloor || 0; + const bidCpm = bidRequest.params.flatCpm; + + if (!serverBid.gam && bidRequest.params.gam) { + // build GAM config + serverBid.gam = JSON.stringify({ + placement: bidRequest.params.gam, + multiplier: 1, + floor: bidRequest.params.gamFloor, + ceil: 100, + namedSizes: ['fluid'], + div: 'div-gpt-ad-x01', + targeting: { + OAS_retarg: '0', + PREBID_ON: '1', + DFPHASH: '', + emptygaf: '0', + }, + }); + } + + const bid = { + requestId: bidRequest.bidId, + creativeId: serverBid.crid || 'mcad_' + request.bidderRequest.auctionId + '_' + request.bidderRequest.params.id, + cpm: bidCpm || serverBid.price, + currency: response.cur, + ttl: serverBid.exp || 300, + width: serverBid.w, + height: serverBid.h, + bidderCode: BIDDER_CODE, + mediaType: 'banner', + meta: { + advertiserDomains: serverBid.adomain, + }, + netRevenue: true, + ad: renderCreative(site, response.id, serverBid, seat, request.bidderRequest), + }; + + if (bid.cpm > 0) { + if (bid.cpm >= bidFloor) { + bids.push(bid); + } else { + utils.logWarn('Discarding bid due to bidFloor setting', bid.cpm, bidFloor); + } + } + } else { + utils.logWarn('Discarding response - no matching request', serverBid.impid); + } + }); + }); + } + + return bids; + }, + getUserSyncs(syncOptions) { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: SYNC_URL, + }]; + } + utils.logWarn('sspBC adapter requires iframe based user sync.'); + }, + onTimeout() { + }, +}; + +registerBidder(spec); + +export { + spec, +}; diff --git a/modules/sspBCAdapter.md b/modules/sspBCAdapter.md new file mode 100644 index 00000000000..645f41fcdc1 --- /dev/null +++ b/modules/sspBCAdapter.md @@ -0,0 +1,40 @@ +# Overview + +Module Name: sspBC Bidder Adapter +Module Type: Bidder Adapter +Maintainer: wojciech.bialy@grupawp.pl + +# Description + +Module that connects to Wirtualna Polska Media header bidding endpoint to fetch bids. +Only banner format is supported. +Supported currencies: USD, EUR, PLN + + +Required parameters: + + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'sspBC', + params: { + id: '006', // required + siteId: '235911', // required + domain: 'somesite.pl', // optional + page: 'somesite.pl/somepage.html', // optional + tmax: 250 // optional + } + }] + } +]; +``` diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 72be98eca50..15399b9b980 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -100,6 +100,12 @@ const USER_IDS_CONFIG = { atype: 1 }, + // merkleId + 'merkleId': { + source: 'merkleinc.com', + atype: 1 + }, + // NetId 'netId': { source: 'netid.de', diff --git a/modules/userId/eids.md b/modules/userId/eids.md index f42b7e61a52..846b9b19207 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -55,6 +55,14 @@ userIdAsEids = [ } }, + { + source: 'merkleinc.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { source: 'britepool.com', uids: [{ diff --git a/modules/userId/index.js b/modules/userId/index.js index afdd93a57ba..14f7ad3599b 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -232,7 +232,7 @@ function getStoredValue(storage, key = undefined) { * @param consentData * @returns {{apiVersion: number, gdprApplies: boolean, consentString: string}} */ -function makeStoredConsentDataObject(consentData) { +function makeStoredConsentDataHash(consentData) { const storedConsentData = { consentString: '', gdprApplies: false, @@ -244,8 +244,7 @@ function makeStoredConsentDataObject(consentData) { storedConsentData.gdprApplies = consentData.gdprApplies; storedConsentData.apiVersion = consentData.apiVersion; } - - return storedConsentData; + return utils.cyrb53Hash(JSON.stringify(storedConsentData)); } /** @@ -255,7 +254,7 @@ function makeStoredConsentDataObject(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, JSON.stringify(makeStoredConsentDataObject(consentData)), expiresStr, 'Lax'); + coreStorage.setCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name, makeStoredConsentDataHash(consentData), expiresStr, 'Lax'); } catch (error) { utils.logError(error); } @@ -266,13 +265,11 @@ export function setStoredConsentData(consentData) { * @returns {string} */ function getStoredConsentData() { - let storedValue; try { - storedValue = JSON.parse(coreStorage.getCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name)); + return coreStorage.getCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name); } catch (e) { utils.logError(e); } - return storedValue; } /** @@ -287,7 +284,7 @@ function storedConsentDataMatchesConsentData(storedConsentData, consentData) { return ( typeof storedConsentData === 'undefined' || storedConsentData === null || - utils.deepEqual(storedConsentData, makeStoredConsentDataObject(consentData)) + storedConsentData === makeStoredConsentDataHash(consentData) ); } diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index b2a9176e342..1d1636bda69 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -68,7 +68,8 @@ export const spec = { return { method: 'GET', url: `${ENDPOINT}/yp/${adslots}?${queryString}`, - validBidRequests: validBidRequests + validBidRequests: validBidRequests, + queryParams: query } }, @@ -80,6 +81,7 @@ export const spec = { interpretResponse: function (serverResponse, originalBidRequest) { const bidResponses = [] const timestamp = Date.now() + const reqParams = originalBidRequest.queryParams originalBidRequest.validBidRequests.forEach(function (bidRequest) { if (!serverResponse.body) { @@ -95,6 +97,8 @@ export const spec = { const customsize = bidRequest.params.adSize !== undefined ? parseSize(bidRequest.params.adSize) : primarysize const extId = bidRequest.params.extId !== undefined ? '&id=' + bidRequest.params.extId : '' const adType = matchedBid.adtype !== undefined ? matchedBid.adtype : '' + const gdprApplies = reqParams.gdpr ? '&gdpr=' + reqParams.gdpr : '' + const gdprConsent = reqParams.consent ? '&consent=' + reqParams.consent : '' const bidResponse = { requestId: bidRequest.bidId, @@ -107,7 +111,7 @@ export const spec = { netRevenue: false, ttl: BID_RESPONSE_TTL_SEC, referrer: '', - ad: `` + ad: `` } if (isVideo(bidRequest, adType)) { @@ -117,7 +121,7 @@ export const spec = { bidResponse.height = playersize[1] } bidResponse.mediaType = VIDEO - bidResponse.vastUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/${customsize[0]}x${customsize[1]}?ts=${timestamp}${extId}` + bidResponse.vastUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/${customsize[0]}x${customsize[1]}?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}` if (isOutstream(bidRequest)) { const renderer = Renderer.install({ diff --git a/modules/yieldoneAnalyticsAdapter.js b/modules/yieldoneAnalyticsAdapter.js index dd40e205b07..542c0917708 100644 --- a/modules/yieldoneAnalyticsAdapter.js +++ b/modules/yieldoneAnalyticsAdapter.js @@ -123,7 +123,7 @@ const yieldoneAnalytics = Object.assign(adapter({analyticsType}), { auctionManager.getAdUnitCodes(), auctionManager.getBidsReceived() ); - if (yieldoneAnalytics.eventsStorage[currentAuctionId]) { + if (yieldoneAnalytics.eventsStorage[currentAuctionId] && yieldoneAnalytics.eventsStorage[currentAuctionId].events.length) { yieldoneAnalytics.eventsStorage[currentAuctionId].page = {url: referrers[currentAuctionId]}; yieldoneAnalytics.eventsStorage[currentAuctionId].pubId = pubId; yieldoneAnalytics.eventsStorage[currentAuctionId].wrapper_version = '$prebid.version$'; @@ -139,8 +139,8 @@ const yieldoneAnalytics = Object.assign(adapter({analyticsType}), { } } }, - sendStat(events, auctionId) { - if (!events) return; + sendStat(data, auctionId) { + if (!data || !data.events || !data.events.length) return; delete yieldoneAnalytics.eventsStorage[auctionId]; ajax( url, @@ -148,7 +148,7 @@ const yieldoneAnalytics = Object.assign(adapter({analyticsType}), { success: function() {}, error: function() {} }, - JSON.stringify(events), + JSON.stringify(data), { method: 'POST' } diff --git a/modules/yuktamediaAnalyticsAdapter.js b/modules/yuktamediaAnalyticsAdapter.js index 3c27ca9754a..caca3bf3341 100644 --- a/modules/yuktamediaAnalyticsAdapter.js +++ b/modules/yuktamediaAnalyticsAdapter.js @@ -4,18 +4,36 @@ import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; import * as utils from '../src/utils.js'; import { getStorageManager } from '../src/storageManager.js'; +import { getRefererInfo } from '../src/refererDetection.js'; const storage = getStorageManager(); -const yuktamediaAnalyticsVersion = 'v3.0.0'; +const yuktamediaAnalyticsVersion = 'v3.1.0'; let initOptions; -let auctionTimestamp; const events = { auctions: {} }; const localStoragePrefix = 'yuktamediaAnalytics_'; const utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; +const location = utils.getWindowLocation(); +const referer = getRefererInfo().referer; +const _pageInfo = { + userAgent: window.navigator.userAgent, + timezoneOffset: new Date().getTimezoneOffset(), + language: window.navigator.language, + screenWidth: window.screen.width, + screenHeight: window.screen.height, + pageViewId: utils.generateUUID(), + host: location.host, + path: location.pathname, + search: location.search, + hash: location.hash, + referer: referer, + refererDomain: utils.parseUrl(referer).host, + yuktamediaAnalyticsVersion: yuktamediaAnalyticsVersion, + prebidVersion: $$PREBID_GLOBAL$$.version +}; function getParameterByName(param) { let vars = {}; @@ -60,20 +78,12 @@ function isUtmTimeoutExpired() { } function send(data, status) { - const location = utils.getWindowLocation(); - data.initOptions = Object.assign({ host: location.host, path: location.pathname, search: location.search }, initOptions); - + data.initOptions = Object.assign(_pageInfo, initOptions); const yuktamediaAnalyticsRequestUrl = utils.buildUrl({ protocol: 'https', hostname: 'analytics-prebid.yuktamedia.com', - pathname: '/api/bids', - search: { - auctionTimestamp: auctionTimestamp, - yuktamediaAnalyticsVersion: yuktamediaAnalyticsVersion, - prebidVersion: $$PREBID_GLOBAL$$.version - } + pathname: '/api/bids' }); - if (isNavigatorSendBeaconSupported()) { window.navigator.sendBeacon(yuktamediaAnalyticsRequestUrl, JSON.stringify(data)); } else { @@ -81,7 +91,7 @@ function send(data, status) { } } -var yuktamediaAnalyticsAdapter = Object.assign(adapter({analyticsType: 'endpoint'}), { +var yuktamediaAnalyticsAdapter = Object.assign(adapter({ analyticsType: 'endpoint' }), { track({ eventType, args }) { if (typeof args !== 'undefined') { switch (eventType) { @@ -89,7 +99,6 @@ var yuktamediaAnalyticsAdapter = Object.assign(adapter({analyticsType: 'endpoint utils.logInfo(localStoragePrefix + 'AUCTION_INIT:', JSON.stringify(args)); if (typeof args.auctionId !== 'undefined' && args.auctionId.length) { events.auctions[args.auctionId] = { bids: {} }; - auctionTimestamp = args.timestamp; } break; case CONSTANTS.EVENTS.BID_REQUESTED: diff --git a/package.json b/package.json index 3cb04dc6f8a..2a0f4077462 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.4.0-pre", + "version": "4.6.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { diff --git a/src/targeting.js b/src/targeting.js index 5873eeeb559..1b1e14fd4a6 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -325,7 +325,10 @@ export function newTargeting(auctionManager) { Object.keys(targetingConfig).filter(customSlotMatching ? customSlotMatching(slot) : isAdUnitCodeMatchingSlot(slot)) .forEach(targetId => Object.keys(targetingConfig[targetId]).forEach(key => { - let valueArr = targetingConfig[targetId][key].split(','); + let valueArr = targetingConfig[targetId][key]; + if (typeof valueArr === 'string') { + valueArr = valueArr.split(','); + } valueArr = (valueArr.length > 1) ? [valueArr] : valueArr; valueArr.map((value) => { utils.logMessage(`Attempting to set key value for slot: ${slot.getSlotElementId()} key: ${key} value: ${value}`); diff --git a/src/utils.js b/src/utils.js index f0fa57c4cff..591c1d1bb2b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1215,3 +1215,43 @@ export function mergeDeep(target, ...sources) { return mergeDeep(target, ...sources); } + +/** + * returns a hash of a string using a fast algorithm + * source: https://stackoverflow.com/a/52171480/845390 + * @param str + * @param seed (optional) + * @returns {string} + */ +export function cyrb53Hash(str, seed = 0) { + // IE doesn't support imul + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul#Polyfill + let imul = function(opA, opB) { + if (isFn(Math.imul)) { + return Math.imul(opA, opB); + } else { + opB |= 0; // ensure that opB is an integer. opA will automatically be coerced. + // floating points give us 53 bits of precision to work with plus 1 sign bit + // automatically handled for our convienence: + // 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001 + // 0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ + var result = (opA & 0x003fffff) * opB; + // 2. We can remove an integer coersion from the statement above because: + // 0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001 + // 0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ + if (opA & 0xffc00000) result += (opA & 0xffc00000) * opB | 0; + return result | 0; + } + }; + + let h1 = 0xdeadbeef ^ seed; + let h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = imul(h1 ^ ch, 2654435761); + h2 = imul(h2 ^ ch, 1597334677); + } + h1 = imul(h1 ^ (h1 >>> 16), 2246822507) ^ imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = imul(h2 ^ (h2 >>> 16), 2246822507) ^ imul(h1 ^ (h1 >>> 13), 3266489909); + return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(); +} diff --git a/test/spec/modules/adheseBidAdapter_spec.js b/test/spec/modules/adheseBidAdapter_spec.js index 0743ddd211d..aa4872641b4 100644 --- a/test/spec/modules/adheseBidAdapter_spec.js +++ b/test/spec/modules/adheseBidAdapter_spec.js @@ -95,6 +95,14 @@ describe('AdheseAdapter', function () { expect(JSON.parse(req.data).parameters).to.deep.include({ 'ci': [ 'london', 'gent' ] }); }); + it('should filter out empty params', function () { + let req = spec.buildRequests([ bidWithParams({ 'aa': [], 'bb': null, 'cc': '', 'dd': [ '', '' ], 'ee': [ 0, 1, null ], 'ff': 0, 'gg': [ 'x', 'y', '' ] }) ], bidderRequest); + + let params = JSON.parse(req.data).parameters; + expect(params).to.not.have.any.keys('aa', 'bb', 'cc', 'dd'); + expect(params).to.deep.include({ 'ee': [ 0, 1 ], 'ff': [ 0 ], 'gg': [ 'x', 'y' ] }); + }); + it('should include gdpr consent param', function () { let req = spec.buildRequests([ minimalBid() ], bidderRequest); diff --git a/test/spec/modules/aniviewBidAdapter_spec.js b/test/spec/modules/aniviewBidAdapter_spec.js index cca175c3388..2e1fdb56201 100644 --- a/test/spec/modules/aniviewBidAdapter_spec.js +++ b/test/spec/modules/aniviewBidAdapter_spec.js @@ -177,6 +177,33 @@ describe('ANIVIEW Bid Adapter Test', function () { let result = spec.interpretResponse(nobidResponse, bidRequest); expect(result.length).to.equal(0); }); + + it('should add renderer if outstream context', function () { + const bidRequest = spec.buildRequests([ + { + bidId: '253dcb69fb2577', + params: { + playerDomain: 'example.com', + AV_PUBLISHERID: '55b78633181f4603178b4568', + AV_CHANNELID: '55b7904d181f46410f8b4568' + }, + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'outstream' + } + } + } + ])[0] + const bidResponse = spec.interpretResponse(serverResponse, bidRequest)[0] + + expect(bidResponse.renderer.url).to.equal('https://example.com/script/6.1/prebidRenderer.js') + expect(bidResponse.renderer.config.AV_PUBLISHERID).to.equal('55b78633181f4603178b4568') + expect(bidResponse.renderer.config.AV_CHANNELID).to.equal('55b7904d181f46410f8b4568') + expect(bidResponse.renderer.loaded).to.equal(false) + expect(bidResponse.width).to.equal(640) + expect(bidResponse.height).to.equal(480) + }) }); describe('getUserSyncs', function () { diff --git a/test/spec/modules/brightMountainMediaBidAdapter_spec.js b/test/spec/modules/brightMountainMediaBidAdapter_spec.js new file mode 100644 index 00000000000..3b7e46e55a7 --- /dev/null +++ b/test/spec/modules/brightMountainMediaBidAdapter_spec.js @@ -0,0 +1,139 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/brightMountainMediaBidAdapter.js'; + +describe('brightMountainMediaBidAdapter_spec', function () { + let bid = { + bidId: '2dd581a2b6281d', + bidder: 'brightmountainmedia', + bidderRequestId: '145e1d6a7837c9', + params: { + placement_id: '123qwerty' + }, + placementCode: 'placementid_0', + auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + transactionId: '3bb2f6da-87a6-4029-aeb0-bfe951372e62', + }; + let bidderRequest = { + bidderCode: 'brightmountainmedia', + auctionId: 'fffffff-ffff-ffff-ffff-ffffffffffff', + bidderRequestId: 'ffffffffffffff', + start: 1472239426002, + auctionStart: 1472239426000, + timeout: 5000, + uspConsent: '1YN-', + refererInfo: { + referer: 'http://www.example.com', + reachedTop: true, + }, + bids: [bid] + } + + describe('isBidRequestValid', function () { + it('Should return true when when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false when required params are not passed', function () { + bid.params = {} + 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://console.brightmountainmedia.com/hb/bid'); + }); + + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + let placements = data['placements']; + for (let i = 0; i < placements.length; i++) { + let placement = placements[i]; + expect(placement).to.have.all.keys('placementId', 'bidId', 'traffic', 'sizes'); + expect(placement.placementId).to.be.a('string'); + expect(placement.bidId).to.be.a('string'); + expect(placement.traffic).to.be.a('string'); + expect(placement.sizes).to.be.an('array'); + } + }); + 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 () { + let resObject = { + body: [{ + requestId: '123', + mediaType: 'banner', + cpm: 0.3, + width: 320, + height: 50, + ad: '

Hello ad

', + ttl: 1000, + creativeId: '123asd', + netRevenue: true, + currency: 'USD' + }] + }; + let serverResponses = spec.interpretResponse(resObject); + it('Returns an array of valid server responses if response object is valid', function () { + expect(serverResponses).to.be.an('array').that.is.not.empty; + for (let i = 0; i < serverResponses.length; i++) { + let dataItem = serverResponses[i]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'mediaType'); + expect(dataItem.requestId).to.be.a('string'); + expect(dataItem.cpm).to.be.a('number'); + expect(dataItem.width).to.be.a('number'); + expect(dataItem.height).to.be.a('number'); + expect(dataItem.ad).to.be.a('string'); + expect(dataItem.ttl).to.be.a('number'); + expect(dataItem.creativeId).to.be.a('string'); + expect(dataItem.netRevenue).to.be.a('boolean'); + expect(dataItem.currency).to.be.a('string'); + expect(dataItem.mediaType).to.be.a('string'); + } + it('Returns an empty array if invalid response is passed', function () { + serverResponses = spec.interpretResponse('invalid_response'); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + }); + + describe('getUserSyncs', function () { + let syncoptionsIframe = { + 'iframeEnabled': 'true' + } + it('should return iframe sync option', function () { + expect(spec.getUserSyncs(syncoptionsIframe)).to.be.an('array').with.lengthOf(1); + expect(spec.getUserSyncs(syncoptionsIframe)[0].type).to.exist; + expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.exist; + expect(spec.getUserSyncs(syncoptionsIframe)[0].type).to.equal('iframe') + expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.equal('https://console.brightmountainmedia.com:4444/cookieSync') + }); + }); +}); diff --git a/test/spec/modules/concertAnalyticsAdapter_spec.js b/test/spec/modules/concertAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..b0aad2f3156 --- /dev/null +++ b/test/spec/modules/concertAnalyticsAdapter_spec.js @@ -0,0 +1,157 @@ +import concertAnalytics from 'modules/concertAnalyticsAdapter.js'; +import { expect } from 'chai'; +const sinon = require('sinon'); +let adapterManager = require('src/adapterManager').default; +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('ConcertAnalyticsAdapter', function() { + let sandbox; + let xhr; + let requests; + let clock; + let timestamp = 1896134400; + let auctionId = '9f894496-10fe-4652-863d-623462bf82b8'; + let timeout = 1000; + + before(function () { + sandbox = sinon.createSandbox(); + xhr = sandbox.useFakeXMLHttpRequest(); + requests = []; + + xhr.onCreate = function (request) { + requests.push(request); + }; + clock = sandbox.useFakeTimers(1896134400); + }); + + after(function () { + sandbox.restore(); + }); + + describe('track', function() { + beforeEach(function () { + sandbox.stub(events, 'getEvents').returns([]); + + adapterManager.enableAnalytics({ + provider: 'concert' + }); + }); + + afterEach(function () { + events.getEvents.restore(); + concertAnalytics.eventsStorage = []; + concertAnalytics.disableAnalytics(); + }); + + it('should catch all events', function() { + sandbox.spy(concertAnalytics, 'track'); + + fireBidEvents(events); + sandbox.assert.callCount(concertAnalytics.track, 5); + }); + + it('should report data for BID_RESPONSE, BID_WON events', function() { + fireBidEvents(events); + clock.tick(3000 + 1000); + + const eventsToReport = ['bidResponse', 'bidWon']; + for (var i = 0; i < concertAnalytics.eventsStorage.length; i++) { + expect(eventsToReport.indexOf(concertAnalytics.eventsStorage[i].event)).to.be.above(-1); + } + + for (var i = 0; i < eventsToReport.length; i++) { + expect(concertAnalytics.eventsStorage.some(function(event) { + return event.event === eventsToReport[i] + })).to.equal(true); + } + }); + + it('should report data in the shape expected by analytics endpoint', function() { + fireBidEvents(events); + clock.tick(3000 + 1000); + + const requiredFields = ['event', 'concert_rid', 'adId', 'auctionId', 'creativeId', 'position', 'url', 'cpm', 'width', 'height', 'timeToRespond']; + + for (var i = 0; i < requiredFields.length; i++) { + expect(concertAnalytics.eventsStorage[0]).to.have.property(requiredFields[i]); + } + }); + }); + + const adUnits = [{ + code: 'desktop_leaderboard_variable', + sizes: [[1030, 590]], + mediaTypes: { + banner: { + sizes: [[1030, 590]] + } + }, + bids: [ + { + bidder: 'concert', + params: { + partnerId: 'test_partner' + } + } + ] + }]; + + const bidResponse = { + 'bidderCode': 'concert', + 'width': 1030, + 'height': 590, + 'statusMessage': 'Bid available', + 'adId': '642f13fe18ab7dc', + 'requestId': '4062fba2e039919', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 6, + 'ad': '', + 'ttl': 360, + 'creativeId': '138308483085|62bac030-a5d3-11ea-b3be-55590c8153a5', + 'netRevenue': false, + 'currency': 'USD', + 'originalCpm': 6, + 'originalCurrency': 'USD', + 'auctionId': '9f894496-10fe-4652-863d-623462bf82b8', + 'responseTimestamp': 1591213790366, + 'requestTimestamp': 1591213790017, + 'bidder': 'concert', + 'adUnitCode': 'desktop_leaderboard_variable', + 'timeToRespond': 349, + 'status': 'rendered', + 'params': [ + { + 'partnerId': 'cst' + } + ] + } + + const bidWon = { + 'adId': '642f13fe18ab7dc', + 'mediaType': 'banner', + 'requestId': '4062fba2e039919', + 'cpm': 6, + 'creativeId': '138308483085|62bac030-a5d3-11ea-b3be-55590c8153a5', + 'currency': 'USD', + 'netRevenue': false, + 'ttl': 360, + 'auctionId': '9f894496-10fe-4652-863d-623462bf82b8', + 'statusMessage': 'Bid available', + 'responseTimestamp': 1591213790366, + 'requestTimestamp': 1591213790017, + 'bidder': 'concert', + 'adUnitCode': 'desktop_leaderboard_variable', + 'sizes': [[1030, 590]], + 'size': [1030, 590] + } + + function fireBidEvents(events) { + events.emit(constants.EVENTS.AUCTION_INIT, {timestamp, auctionId, timeout, adUnits}); + events.emit(constants.EVENTS.BID_REQUESTED, {bidder: 'concert'}); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponse); + events.emit(constants.EVENTS.AUCTION_END, {}); + events.emit(constants.EVENTS.BID_WON, bidWon); + } +}); diff --git a/test/spec/modules/concertBidAdapter_spec.js b/test/spec/modules/concertBidAdapter_spec.js new file mode 100644 index 00000000000..df999f45df9 --- /dev/null +++ b/test/spec/modules/concertBidAdapter_spec.js @@ -0,0 +1,219 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { spec } from 'modules/concertBidAdapter.js'; +import { getStorageManager } from '../../../src/storageManager.js' + +describe('ConcertAdapter', function () { + let bidRequests; + let bidRequest; + let bidResponse; + + beforeEach(function () { + bidRequests = [ + { + bidder: 'concert', + params: { + partnerId: 'foo', + slotType: 'fizz' + }, + adUnitCode: 'desktop_leaderboard_variable', + bidId: 'foo', + transactionId: '', + sizes: [[1030, 590]] + } + ]; + + bidRequest = { + refererInfo: { + referer: 'https://www.google.com' + }, + uspConsent: '1YYY', + gdprConsent: {} + }; + + bidResponse = { + body: { + bids: [ + { + bidId: '16d2e73faea32d9', + cpm: '6', + width: '1030', + height: '590', + ad: '', + ttl: '360', + creativeId: '123349|a7d62700-a4bf-11ea-829f-ad3b0b7a9383', + netRevenue: false, + currency: 'USD' + } + ] + } + } + }); + + describe('spec.isBidRequestValid', function() { + it('should return when it recieved all the required params', function() { + const bid = bidRequests[0]; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when partner id is missing', function() { + const bid = { + bidder: 'concert', + params: {} + } + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('spec.buildRequests', function() { + it('should build a payload object with the shape expected by server', function() { + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + expect(payload).to.have.property('meta'); + expect(payload).to.have.property('slots'); + + const metaRequiredFields = ['prebidVersion', 'pageUrl', 'screen', 'debug', 'uid', 'optedOut', 'adapterVersion', 'uspConsent', 'gdprConsent']; + const slotsRequiredFields = ['name', 'bidId', 'transactionId', 'sizes', 'partnerId', 'slotType']; + + metaRequiredFields.forEach(function(field) { + expect(payload.meta).to.have.property(field); + }); + slotsRequiredFields.forEach(function(field) { + expect(payload.slots[0]).to.have.property(field); + }); + }); + + it('should not generate uid if the user has opted out', function() { + const storage = getStorageManager(); + storage.setDataInLocalStorage('c_nap', 'true'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.uid).to.equal(false); + }); + + it('should generate uid if the user has not opted out', function() { + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.uid).to.not.equal(false); + }); + + it('should grab uid from local storage if it exists', function() { + const storage = getStorageManager(); + storage.setDataInLocalStorage('c_uid', 'foo'); + storage.removeDataFromLocalStorage('c_nap'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.uid).to.equal('foo'); + }); + }); + + describe('spec.interpretResponse', function() { + it('should return bids in the shape expected by prebid', function() { + const bids = spec.interpretResponse(bidResponse, bidRequest); + const requiredFields = ['requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', 'netRevenue', 'currency']; + + requiredFields.forEach(function(field) { + expect(bids[0]).to.have.property(field); + }); + }); + + it('should return empty bids if there is no response from server', function() { + const bids = spec.interpretResponse({ body: null }, bidRequest); + expect(bids).to.have.lengthOf(0); + }); + + it('should return empty bids if there are no bids from the server', function() { + const bids = spec.interpretResponse({ body: {bids: []} }, bidRequest); + expect(bids).to.have.lengthOf(0); + }); + }); + + describe('spec.getUserSyncs', function() { + it('should not register syncs when iframe is not enabled', function() { + const opts = { + iframeEnabled: false + } + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync).to.have.lengthOf(0); + }); + + it('should not register syncs when the user has opted out', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.setDataInLocalStorage('c_nap', 'true'); + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync).to.have.lengthOf(0); + }); + + it('should set gdprApplies flag to 1 if the user is in area where GDPR applies', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: true + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('gdpr_applies=1'); + }); + + it('should set gdprApplies flag to 1 if the user is in area where GDPR applies', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: false + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('gdpr_applies=0'); + }); + + it('should set gdpr consent param with the user\'s choices on consent', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: false, + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==' + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('gdpr_consent=BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + }); + + it('should set ccpa consent param with the user\'s choices on consent', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: false, + uspConsent: '1YYY' + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('usp_consent=1YY'); + }); + }); +}); diff --git a/test/spec/modules/connectadBidAdapter_spec.js b/test/spec/modules/connectadBidAdapter_spec.js index 626018241c4..aef4fb562a7 100644 --- a/test/spec/modules/connectadBidAdapter_spec.js +++ b/test/spec/modules/connectadBidAdapter_spec.js @@ -14,7 +14,8 @@ describe('ConnectAd Adapter', function () { bidder: 'conntectad', params: { siteId: 123456, - networkId: 123456 + networkId: 123456, + bidfloor: 0.50 }, adUnitCode: '/19968336/header-bid-tag-1', mediaTypes: { @@ -46,8 +47,7 @@ describe('ConnectAd Adapter', function () { bidderRequestId: '1c56ad30b9b8ca8', transactionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', userId: { - tdid: '123456', - digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}} + tdid: '123456' } }]; @@ -154,6 +154,23 @@ describe('ConnectAd Adapter', function () { expect(requestparse.placements[0].networkId).to.equal(123456); }); + it('should process floors module if available', function() { + const floorInfo = { + currency: 'USD', + floor: 5.20 + }; + bidRequests[0].getFloor = () => floorInfo; + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.placements[0].floor).to.equal(5.20); + }); + + it('should be 0 if no floormodule is available', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.placements[0].floor).to.equal(0); + }); + it('should contain gdpr info', function () { const request = spec.buildRequests(bidRequests, bidderRequest); const requestparse = JSON.parse(request.data); @@ -227,7 +244,6 @@ describe('ConnectAd Adapter', function () { const requestparse = JSON.parse(request.data); expect(requestparse.user.ext.eids).to.be.an('array'); expect(requestparse.user.ext.eids[0].uids[0].id).to.equal('123456'); - expect(requestparse.user.ext.digitrust.id).to.equal('DTID'); }); it('should add referer info', function () { diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 3ebebfef1ee..deaacbc5a28 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -643,6 +643,32 @@ describe('consentManagement', function () { expect(consent.apiVersion).to.equal(2); }); + it('performs lookup check and stores consentData for a valid existing user with additional consent', function () { + let testConsentData = { + tcString: 'abc12345234', + addtlConsent: 'superduperstring', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded' + }; + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + setConsentConfig(goodConfigWithAllowAuction); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consent = gdprDataHandler.getConsentData(); + sinon.assert.notCalled(utils.logError); + expect(didHookReturn).to.be.true; + expect(consent.consentString).to.equal(testConsentData.tcString); + expect(consent.addtlConsent).to.equal(testConsentData.addtlConsent); + expect(consent.gdprApplies).to.be.true; + expect(consent.apiVersion).to.equal(2); + }); + it('throws an error when processCmpData check fails + does not call requestBids callbcack even when allowAuction is true', function () { let testConsentData = {}; let bidsBackHandlerReturn = false; diff --git a/test/spec/modules/cpmstarBidAdapter_spec.js b/test/spec/modules/cpmstarBidAdapter_spec.js old mode 100755 new mode 100644 index 1993f28e5d5..285fca9690a --- a/test/spec/modules/cpmstarBidAdapter_spec.js +++ b/test/spec/modules/cpmstarBidAdapter_spec.js @@ -3,6 +3,41 @@ import { spec } from 'modules/cpmstarBidAdapter.js'; import { deepClone } from 'src/utils.js'; import { config } from 'src/config.js'; +const valid_bid_requests = [{ + 'bidder': 'cpmstar', + 'params': { + 'placementId': '57' + }, + 'sizes': [[300, 250]], + 'bidId': 'bidId' +}]; + +const bidderRequest = { + refererInfo: { + referer: 'referer', + reachedTop: false, + } +}; + +const serverResponse = { + body: [{ + creatives: [{ + cpm: 1, + width: 0, + height: 0, + currency: 'USD', + netRevenue: true, + ttl: 1, + creativeid: '1234', + requestid: '11123', + code: 'no idea', + media: 'banner', + } + ], + syncs: [{ type: 'image', url: 'https://server.cpmstar.com/pixel.aspx' }] + }] +}; + describe('Cpmstar Bid Adapter', function () { describe('isBidRequestValid', function () { it('should return true since the bid is valid', @@ -42,23 +77,6 @@ describe('Cpmstar Bid Adapter', function () { }); describe('buildRequests', function () { - const valid_bid_requests = [{ - 'bidder': 'cpmstar', - 'params': { - 'placementId': '57' - }, - 'sizes': [[300, 250]], - 'bidId': 'bidId' - }]; - - const bidderRequest = { - refererInfo: { - referer: 'referer', - reachedTop: false, - } - - }; - it('should produce a valid production request', function () { var requests = spec.buildRequests(valid_bid_requests, bidderRequest); expect(requests[0]).to.have.property('method'); @@ -109,7 +127,30 @@ describe('Cpmstar Bid Adapter', function () { expect(requests[0]).to.have.property('url'); expect(requests[0].url).to.include('tfcd=1'); }); - }) + }); + + it('should produce a request with support for OpenRTB SupplyChain', function () { + var reqs = deepClone(valid_bid_requests); + reqs[0].schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1 + }, + { + 'asi': 'exchange2.com', + 'sid': 'abcd', + 'hp': 1 + } + ] + }; + var requests = spec.buildRequests(reqs, bidderRequest); + expect(requests[0]).to.have.property('url'); + expect(requests[0].url).to.include('&schain=1.0,1!exchange1.com,1234,1,,,!exchange2.com,abcd,1,,,'); + }); describe('interpretResponse', function () { const request = { @@ -117,23 +158,6 @@ describe('Cpmstar Bid Adapter', function () { mediaType: 'BANNER' } }; - const serverResponse = { - body: [{ - creatives: [{ - cpm: 1, - width: 0, - height: 0, - currency: 'USD', - netRevenue: true, - ttl: 1, - creativeid: '1234', - requestid: '11123', - code: 'no idea', - media: 'banner', - } - ], - }] - }; it('should return a valid bidresponse array', function () { var r = spec.interpretResponse(serverResponse, request) @@ -185,4 +209,23 @@ describe('Cpmstar Bid Adapter', function () { expect(spec.interpretResponse(dealServer, request)[0].dealId).to.equal('deal'); }); }); + + describe('getUserSyncs', function () { + var sres = [deepClone(serverResponse)]; + + it('should return a valid pixel sync', function () { + var syncs = spec.getUserSyncs({ pixelEnabled: true }, sres); + expect(syncs.length).equal(1); + expect(syncs[0].type).equal('image'); + expect(syncs[0].url).equal('https://server.cpmstar.com/pixel.aspx'); + }); + + it('should return a valid iframe sync', function () { + sres[0].body[0].syncs[0].type = 'iframe'; + var syncs = spec.getUserSyncs({ iframeEnabled: true }, sres); + expect(syncs.length).equal(1); + expect(syncs[0].type).equal('iframe'); + expect(syncs[0].url).equal('https://server.cpmstar.com/pixel.aspx'); + }); + }); }); diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index cd412530d2b..c0ecb9cad5e 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -38,7 +38,7 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('env', 'vp'); expect(queryParams).to.have.property('gdfp_req', '1'); expect(queryParams).to.have.property('iu', 'my/adUnit'); - expect(queryParams).to.have.property('output', 'xml_vast3'); + expect(queryParams).to.have.property('output', 'vast'); expect(queryParams).to.have.property('sz', '640x480'); expect(queryParams).to.have.property('unviewed_position_start', '1'); expect(queryParams).to.have.property('url'); @@ -375,7 +375,7 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('env', 'vp'); expect(queryParams).to.have.property('gdfp_req', '1'); expect(queryParams).to.have.property('iu', 'my/adUnit'); - expect(queryParams).to.have.property('output', 'xml_vast3'); + expect(queryParams).to.have.property('output', 'vast'); expect(queryParams).to.have.property('sz', '640x480'); expect(queryParams).to.have.property('unviewed_position_start', '1'); expect(queryParams).to.have.property('url'); diff --git a/test/spec/modules/districtmDmxBidAdapter_spec.js b/test/spec/modules/districtmDmxBidAdapter_spec.js index d8f0beb9a36..9b65ab855d8 100644 --- a/test/spec/modules/districtmDmxBidAdapter_spec.js +++ b/test/spec/modules/districtmDmxBidAdapter_spec.js @@ -1,6 +1,60 @@ import {expect} from 'chai'; import * as _ from 'lodash'; -import {spec, matchRequest, checkDeepArray, defaultSize, upto5, cleanSizes, shuffle} from '../../../modules/districtmDMXBidAdapter.js'; +import {spec, matchRequest, checkDeepArray, defaultSize, upto5, cleanSizes, shuffle, getApi, bindUserId, getPlaybackmethod, getProtocols, cleanVast} from '../../../modules/districtmDMXBidAdapter.js'; + +const sample_vast = ` + + + + + + + + + 00:00:15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` const supportedSize = [ { @@ -42,6 +96,28 @@ const bidRequest = [{ 'dmxid': 100001, 'memberid': 100003, }, + 'userId': { + idl_env: {}, + digitrustid: { + data: { + id: {} + } + }, + id5id: {}, + pubcid: {}, + tdid: {}, + criteoId: {}, + britepoolid: {}, + intentiqid: {}, + lotamePanoramaId: {}, + parrableId: {}, + netId: {}, + sharedid: {}, + lipb: { + lipbid: {} + }, + + }, 'adUnitCode': 'div-gpt-ad-12345678-1', 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', 'sizes': [ @@ -53,6 +129,31 @@ const bidRequest = [{ 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' }]; +const bidRequestVideo = [{ + 'bidder': 'districtmDMX', + 'params': { + 'dmxid': 100001, + 'memberid': 100003, + 'video': { + id: 123, + skipppable: true, + playback_method: ['auto_play_sound_off', 'viewport_sound_off'], + mimes: ['application/javascript', + 'video/mp4'], + } + }, + 'mediaTypes': { video: {context: 'instream', // or 'outstream' + playerSize: [[640, 480]]} }, + 'adUnitCode': 'div-gpt-ad-12345678-1', + 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', + 'sizes': [ + [300, 250], + [300, 600] + ], + 'bidId': '29a28a1bbc8a8d', + 'bidderRequestId': '124b579a136515', + 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' +}]; const bidRequestNoCoppa = [{ 'bidder': 'districtmDMX', 'params': { @@ -505,6 +606,51 @@ describe('DistrictM Adaptor', function () { expect(upto5([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], bidRequest, bidderRequest, 'https://google').length).to.be.equal(3) }) }) + + describe('test vast tag', function() { + it('img tag should not be present', function() { + expect(cleanVast(sample_vast).indexOf('img') !== -1).to.be.equal(false) + }) + }) + describe('Test getApi function', function() { + const data = { + protocols: ['VPAID_1_0'] + } + it('Will return 1 for vpaid version 1', function() { + expect(getApi(data)[0]).to.be.equal(1) + }) + it('Will return 2 for vpaid default', function() { + expect(getApi({})[0]).to.be.equal(2) + }) + }) + + describe('Test cleanSizes function', function() { + it('sequence will be respected', function() { + expect(cleanSizes(bidderRequest.bids[0].sizes).toString()).to.be.equal('300,250,300,600') + }) + it('sequence will be respected', function() { + expect(cleanSizes([[728, 90], [970, 90], [300, 600], [320, 50]]).toString()).to.be.equal('728,90,320,50,300,600,970,90') + }) + }) + + describe('Test getPlaybackmethod function', function() { + it('getPlaybackmethod will return 2', function() { + expect(getPlaybackmethod([])[0]).to.be.equal(2) + }) + it('getPlaybackmethod will return 6', function() { + expect(getPlaybackmethod(['viewport_sound_off'])[0]).to.be.equal(6) + }) + }) + + describe('Test getProtocols function', function() { + it('getProtocols will return 3', function() { + expect(getProtocols({protocols: ['VAST_3_0']})[0]).to.be.equal(3) + }) + it('getProtocols will return 6', function() { + expect(_.isEqual(getProtocols({}), [2, 3, 5, 6, 7, 8])).to.be.equal(true) + }) + }) + describe('All needed functions are available', function () { it(`isBidRequestValid is present and type function`, function () { expect(districtm.isBidRequestValid).to.exist.and.to.be.a('function') @@ -589,6 +735,12 @@ describe('DistrictM Adaptor', function () { }); }); + describe('bidRequest Video testing', function() { + const request = districtm.buildRequests(bidRequestVideo, bidRequestVideo); + const data = JSON.parse(request.data) + expect(data instanceof Object).to.be.equal(true) + }) + describe(`interpretResponse test usage`, function () { const responseResults = districtm.interpretResponse(responses, {bidderRequest}); const emptyResponseResults = districtm.interpretResponse(emptyResponse, {bidderRequest}); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 434e1841a0f..8ad44f0b1ad 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -55,6 +55,18 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('merkleId', function() { + const userId = { + merkleId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'merkleinc.com', + uids: [{id: 'some-random-id-value', atype: 1}] + }); + }); + it('identityLink', function() { const userId = { idl_env: 'some-random-id-value' diff --git a/test/spec/modules/emx_digitalBidAdapter_spec.js b/test/spec/modules/emx_digitalBidAdapter_spec.js index 7be8a2ce5ac..138786b9c74 100644 --- a/test/spec/modules/emx_digitalBidAdapter_spec.js +++ b/test/spec/modules/emx_digitalBidAdapter_spec.js @@ -612,12 +612,23 @@ describe('emx_digital Adapter', function () { }); describe('getUserSyncs', function () { - let syncOptionsIframe = { iframeEnabled: true }; - let syncOptionsPixel = { pixelEnabled: true }; - it('Should push the correct sync type depending on the config', function () { - let iframeSync = spec.getUserSyncs(syncOptionsIframe); - expect(iframeSync.length).to.equal(1); - expect(iframeSync[0].type).to.equal('iframe'); + it('should register the iframe sync url', function () { + let syncs = spec.getUserSyncs({ + iframeEnabled: true + }); + expect(syncs).to.not.be.an('undefined'); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + }); + + it('should pass gdpr params', function () { + let syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, { + gdprApplies: false, consentString: 'test' + }); + 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=0'); }); }); }); diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index 0d539d5988c..9f36ba92558 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -41,7 +41,22 @@ describe('IdentityLinkId tests', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the LiveRamp envelope endpoint with consent string', function () { + it('should NOT call the LiveRamp envelope endpoint if gdpr applies but consent string is empty string', function () { + let consentData = { + gdprApplies: true, + consentString: '' + }; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData); + expect(submoduleCallback).to.be.undefined; + }); + + it('should NOT call the LiveRamp envelope endpoint if gdpr applies but consent string is missing', function () { + let consentData = { gdprApplies: true }; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData); + expect(submoduleCallback).to.be.undefined; + }); + + it('should call the LiveRamp envelope endpoint with IAB consent string v1', function () { let callBackSpy = sinon.spy(); let consentData = { gdprApplies: true, @@ -59,6 +74,27 @@ describe('IdentityLinkId tests', function () { expect(callBackSpy.calledOnce).to.be.true; }); + it('should call the LiveRamp envelope endpoint with IAB consent string v2', function () { + let callBackSpy = sinon.spy(); + let consentData = { + gdprApplies: true, + consentString: 'CO4VThZO4VTiuADABBENAzCgAP_AAEOAAAAAAwwAgAEABhAAgAgAAA.YAAAAAAAAAA', + vendorData: { + tcfPolicyVersion: 2 + } + }; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14&ct=4&cv=CO4VThZO4VTiuADABBENAzCgAP_AAEOAAAAAAwwAgAEABhAAgAgAAA.YAAAAAAAAAA'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + it('should not throw Uncaught TypeError when envelope endpoint returns empty response', function () { let callBackSpy = sinon.spy(); let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; diff --git a/test/spec/modules/invibesBidAdapter_spec.js b/test/spec/modules/invibesBidAdapter_spec.js index b75c6a4578f..442039504f8 100644 --- a/test/spec/modules/invibesBidAdapter_spec.js +++ b/test/spec/modules/invibesBidAdapter_spec.js @@ -137,11 +137,19 @@ describe('invibesBidAdapter:', function () { }); it('has capped ids if local storage variable is correctly formatted', function () { + top.window.invibes.optIn = 1; + top.window.invibes.purposes = [true, false, false, false, false, false, false, false, false, false]; localStorage.ivvcap = '{"9731":[1,1768600800000]}'; const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); expect(request.data.capCounts).to.equal('9731=1'); }); + it('does not have capped ids if local storage variable is correctly formatted but no opt in', function () { + localStorage.ivvcap = '{"9731":[1,1768600800000]}'; + const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); + expect(request.data.capCounts).to.equal(''); + }); + it('does not have capped ids if local storage variable is incorrectly formatted', function () { localStorage.ivvcap = ':[1,1574334216992]}'; const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); @@ -178,104 +186,102 @@ describe('invibesBidAdapter:', function () { expect(JSON.parse(request.data.bidParamsJson).placementIds).to.contain(bidRequests[1].params.placementId); }); - it('uses cookies', function () { - global.document.cookie = 'ivNoCookie=1'; + it('sends undefined lid when no cookie', function () { let request = spec.buildRequests(bidRequests); expect(request.data.lId).to.be.undefined; }); - it('doesnt send the domain id if not graduated', function () { - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":1522929537626,"hc":1}'; - let request = spec.buildRequests(bidRequests); - expect(request.data.lId).to.not.exist; - }); - - it('try to graduate but not enough count - doesnt send the domain id', function () { - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":1521818537626,"hc":0}'; - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - }); - - it('try to graduate but not old enough - doesnt send the domain id', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":' + Date.now() + ',"hc":5}'; - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - }); - - it('graduate and send the domain id', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - stubDomainOptions(new StubbedPersistence('{"id":"dvdjkams6nkq","cr":1521818537626,"hc":7}')); - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.exist; - }); - - it('send the domain id if already graduated', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi"}')); - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.exist; - expect(top.window.invibes.dom.tempId).to.exist; - }); - - it('send the domain id after replacing it with new format', function () { + it('sends lid when comes on cookie', function () { + top.window.invibes.optIn = 1; + top.window.invibes.purposes = [true, false, false, false, false, false, false, false, false, false]; + global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":' + Date.now() + ',"hc":0}'; let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.lId).to.exist; - expect(top.window.invibes.dom.tempId).to.exist; }); - it('dont send the domain id if consent declined on tcf v1', function () { + it('should send purpose 1', function () { let bidderRequest = { gdprConsent: { vendorData: { gdprApplies: true, hasGlobalConsent: false, - vendorConsents: {436: false} + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } } } }; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.not.exist; - expect(request.data.oi).to.equal(0); + expect(request.data.purposes.split(',')[0]).to.equal('true'); }); - it('dont send the domain id if consent declined on tcf v2', function () { + it('should send purpose 2 & 7', function () { let bidderRequest = { gdprConsent: { vendorData: { gdprApplies: true, hasGlobalConsent: false, - vendor: {consents: {436: false}} + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } } } }; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.not.exist; - expect(request.data.oi).to.equal(0); - }); - - it('dont send the domain id if no consent', function () { - let bidderRequest = {}; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.not.exist; + expect(request.data.purposes.split(',')[1] && request.data.purposes.split(',')[6]).to.equal('true'); }); - it('try to init id but was already loaded on page - does not increment the id again', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":1521818537626,"hc":0}'; + it('should send purpose 9', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } + } + } + }; let request = spec.buildRequests(bidRequests, bidderRequest); - request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.exist; + expect(request.data.purposes.split(',')[9]).to.equal('true'); }); it('should send oi = 0 when vendorData is null', function () { @@ -344,7 +350,7 @@ describe('invibesBidAdapter:', function () { expect(request.data.oi).to.equal(0); }); - it('should send oi = 0 when purpose consents weren\'t approved on tcf v2', function () { + it('should send oi = 2 when purpose consents weren\'t approved on tcf v2', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -369,10 +375,10 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); - it('should send oi = 0 when purpose consents are less then 10 on tcf v2', function () { + it('should send oi = 2 when purpose consents are less then 10 on tcf v2', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -392,7 +398,7 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); it('should send oi = 4 when vendor consents are null on tcf v2', function () { @@ -527,7 +533,7 @@ describe('invibesBidAdapter:', function () { expect(request.data.oi).to.equal(2); }); - it('should send oi = 0 when purpose consents weren\'t approved on tcf v1', function () { + it('should send oi = 2 when purpose consents weren\'t approved on tcf v1', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -545,10 +551,10 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); - it('should send oi = 0 when purpose consents are less then 5 on tcf v1', function () { + it('should send oi = 2 when purpose consents are less then 5 on tcf v1', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -564,7 +570,7 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); it('should send oi = 0 when vendor consents for invibes are false on tcf v1', function () { @@ -669,8 +675,8 @@ describe('invibesBidAdapter:', function () { context('when the response is valid', function () { it('responds with a valid bid', function () { - top.window.invibes.setCookie('a', 'b', 370); - top.window.invibes.setCookie('c', 'd', 0); + // top.window.invibes.setCookie('a', 'b', 370); + // top.window.invibes.setCookie('c', 'd', 0); let result = spec.interpretResponse({body: response}, {bidRequests}); expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); @@ -705,6 +711,16 @@ describe('invibesBidAdapter:', function () { expect(response.url).to.include(SYNC_ENDPOINT); expect(response.url).to.include('optIn'); expect(response.url).to.include('ivvbks'); + }); + + it('returns an iframe with params including if enabled', function () { + top.window.invibes.optIn = 1; + global.document.cookie = 'ivvbks=17639.0,1,2;ivbsdid={"id":"dvdjkams6nkq","cr":' + Date.now() + ',"hc":0}'; + let response = spec.getUserSyncs({iframeEnabled: true}); + expect(response.type).to.equal('iframe'); + expect(response.url).to.include(SYNC_ENDPOINT); + expect(response.url).to.include('optIn'); + expect(response.url).to.include('ivvbks'); expect(response.url).to.include('ivbsdid'); }); diff --git a/test/spec/modules/ironsourceBidAdapter_spec.js b/test/spec/modules/ironsourceBidAdapter_spec.js new file mode 100644 index 00000000000..cfdc51e0235 --- /dev/null +++ b/test/spec/modules/ironsourceBidAdapter_spec.js @@ -0,0 +1,339 @@ +import { expect } from 'chai'; +import { spec } from 'modules/ironsourceBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { VIDEO } from '../../../src/mediaTypes.js'; + +const ENDPOINT = 'https://hb.yellowblue.io/hb'; +const TTL = 360; + +describe('ironsourceAdapter', 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': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'isOrg': 'jdye8weeyirk00000001' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'isOrg': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'isOrg': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const bidderRequest = { + bidderCode: 'ironsource', + } + + it('sends bid request to ENDPOINT via GET', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('should send the correct bid Id', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.bid_id).to.equal('299ffc8cca0b87'); + } + }); + + it('should send the correct width and height', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('width', 640); + expect(request.data).to.have.property('height', 480); + } + }); + + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.setConfig({ + userSync: { + syncEnabled: true + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'pixel'); + } + }); + + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithUSP); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('us_privacy', '1YNN'); + } + }); + + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('us_privacy'); + } + }); + + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('gdpr'); + expect(request.data).to.not.have.property('gdpr_consent'); + } + }); + + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('gdpr', true); + expect(request.data).to.have.property('gdpr_consent', 'test-consent-string'); + } + }); + + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('schain', '1.0,1!indirectseller.com,00001,,,,'); + } + }); + }); + + describe('interpretResponse', function () { + const response = { + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + netRevenue: true, + currency: 'USD' + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + requestId: '21e12606d47ba7', + cpm: 12.5, + width: 640, + height: 480, + creativeId: '21e12606d47ba7', + currency: 'USD', + netRevenue: true, + ttl: TTL, + vastXml: '', + mediaType: VIDEO + } + ]; + const result = spec.interpretResponse({ body: [response] }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + }; + + const iframeSyncResponse = { + body: { + userSyncURL: 'https://iframe-sync-url.test' + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); + + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); + }); + }) +}); diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js new file mode 100644 index 00000000000..b5bacdc3694 --- /dev/null +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -0,0 +1,399 @@ +import { fetchTargetingForMediaId, getTargetingForBid, + fetchTargetingInformation, jwplayerSubmodule } from 'modules/jwplayerRtdProvider.js'; +import { server } from 'test/mocks/xhr.js'; + +describe('jwplayerRtdProvider', function() { + const testIdForSuccess = 'test_id_for_success'; + const testIdForFailure = 'test_id_for_failure'; + const validSegments = ['test_seg_1', 'test_seg_2']; + const responseHeader = {'Content-Type': 'application/json'}; + + describe('Fetch targeting for mediaID tests', function () { + let request; + + describe('Fetch succeeds', function () { + beforeEach(function () { + fetchTargetingForMediaId(testIdForSuccess); + request = server.requests[0]; + }); + + afterEach(function () { + server.respond(); + }); + + it('should reach out to media endpoint', function () { + expect(request.url).to.be.eq(`https://cdn.jwplayer.com/v2/media/${testIdForSuccess}`); + }); + + it('should write to cache when successful', function () { + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4', + jwpseg: validSegments + } + ] + }) + ); + + const targetingInfo = getTargetingForBid({ + jwTargeting: { + mediaID: testIdForSuccess + } + }); + + const validTargeting = { + segments: validSegments, + mediaID: testIdForSuccess + }; + + expect(targetingInfo).to.deep.equal(validTargeting); + }); + }); + + describe('Fetch fails', function () { + beforeEach(function () { + fetchTargetingForMediaId(testIdForFailure); + request = server.requests[0] + }); + + it('should not write to cache when response is malformed', function() { + request.respond('{]'); + const targetingInfo = getTargetingForBid({ + jwTargeting: { + mediaID: testIdForFailure + } + }); + expect(targetingInfo).to.be.null; + }); + + it('should not write to cache when playlist is absent', function() { + request.respond({}); + const targetingInfo = getTargetingForBid({ + jwTargeting: { + mediaID: testIdForFailure + } + }); + expect(targetingInfo).to.be.null; + }); + + it('should not write to cache when segments are absent', function() { + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4' + } + ] + }) + ); + const targetingInfo = getTargetingForBid({ + jwTargeting: { + mediaID: testIdForFailure + } + }); + expect(targetingInfo).to.be.null; + }); + + it('should not write to cache when request errors', function() { + request.error(); + const targetingInfo = getTargetingForBid({ + jwTargeting: { + mediaID: testIdForFailure + } + }); + expect(targetingInfo).to.be.null; + }); + }); + }); + + describe('Get targeting for bid', function() { + const mediaIdWithSegment = 'media_ID_1'; + const mediaIdNoSegment = 'media_ID_2'; + const mediaIdForCurrentItem = 'media_ID_current'; + const mediaIdNotCached = 'media_test_ID'; + + const validPlayerID = 'player_test_ID_valid'; + const invalidPlayerID = 'player_test_ID_invalid'; + + it('returns null when targeting block is missing', function () { + const targeting = getTargetingForBid({}); + expect(targeting).to.be.null; + }); + + it('returns null when jwplayer.js is absent from page', function () { + const targeting = getTargetingForBid({ + jwTargeting: { + playerID: invalidPlayerID, + mediaID: mediaIdNotCached + } + }); + expect(targeting).to.be.null; + }); + + describe('When jwplayer.js is on page', function () { + const playlistItemWithSegmentMock = { + mediaid: mediaIdWithSegment, + jwpseg: validSegments + }; + + const targetingForMediaWithSegment = { + segments: validSegments, + mediaID: mediaIdWithSegment + }; + + const playlistItemNoSegmentMock = { + mediaid: mediaIdNoSegment + }; + + const currentItemSegments = ['test_seg_3', 'test_seg_4']; + const currentPlaylistItemMock = { + mediaid: mediaIdForCurrentItem, + jwpseg: currentItemSegments + }; + const targetingForCurrentItem = { + segments: currentItemSegments, + mediaID: mediaIdForCurrentItem + }; + + const playerInstanceMock = { + getPlaylist: function () { + return [playlistItemWithSegmentMock, playlistItemNoSegmentMock]; + }, + + getPlaylistItem: function () { + return currentPlaylistItemMock; + } + }; + + const jwplayerMock = function(playerID) { + if (playerID === validPlayerID) { + return playerInstanceMock; + } else { + return {}; + } + }; + + beforeEach(function () { + window.jwplayer = jwplayerMock; + }); + + it('returns null when player ID does not match player on page', function () { + const targeting = getTargetingForBid({ + jwTargeting: { + playerID: invalidPlayerID, + mediaID: mediaIdNotCached + } + }); + expect(targeting).to.be.null; + }); + + it('returns segments when media ID matches a playlist item with segments', function () { + const targeting = getTargetingForBid({ + jwTargeting: { + playerID: validPlayerID, + mediaID: mediaIdWithSegment + } + }); + expect(targeting).to.deep.equal(targetingForMediaWithSegment); + }); + + it('caches segments media ID matches a playist item with segments', function () { + getTargetingForBid({ + jwTargeting: { + playerID: validPlayerID, + mediaID: mediaIdWithSegment + } + }); + + window.jwplayer = null; + const targeting2 = getTargetingForBid({ + jwTargeting: { + playerID: invalidPlayerID, + mediaID: mediaIdWithSegment + } + }); + expect(targeting2).to.deep.equal(targetingForMediaWithSegment); + }); + + it('returns segments of current item when media ID is missing', function () { + const targeting = getTargetingForBid({ + jwTargeting: { + playerID: validPlayerID + } + }); + expect(targeting).to.deep.equal(targetingForCurrentItem); + }); + + it('caches segments from the current item', function () { + getTargetingForBid({ + jwTargeting: { + playerID: validPlayerID + } + }); + + window.jwplayer = null; + const targeting2 = getTargetingForBid({ + jwTargeting: { + playerID: invalidPlayerID, + mediaID: mediaIdForCurrentItem + } + }); + expect(targeting2).to.deep.equal(targetingForCurrentItem); + }); + + it('returns undefined segments when segments are absent', function () { + const targeting = getTargetingForBid({ + jwTargeting: { + playerID: validPlayerID, + mediaID: mediaIdNoSegment + } + }); + expect(targeting).to.deep.equal({ + mediaID: mediaIdNoSegment, + segments: undefined + }); + }); + }); + }); + + describe('jwplayerSubmodule', function () { + it('successfully instantiates', function () { + expect(jwplayerSubmodule.init()).to.equal(true); + }); + + describe('getData', function () { + const validMediaIDs = ['media_ID_1', 'media_ID_2', 'media_ID_3']; + let bidRequestSpy; + let fakeServer; + let clock; + + beforeEach(function () { + bidRequestSpy = sinon.spy(); + + fakeServer = sinon.createFakeServer(); + fakeServer.respondImmediately = false; + fakeServer.autoRespond = false; + + clock = sinon.useFakeTimers(); + }); + + afterEach(function () { + clock.restore(); + fakeServer.respond(); + }); + + it('executes callback immediately when no requests are pending', function () { + fetchTargetingInformation({ + mediaIDs: [] + }); + jwplayerSubmodule.getData([], bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('executes callback after requests complete', function() { + fetchTargetingInformation({ + mediaIDs: validMediaIDs + }); + jwplayerSubmodule.getData([], bidRequestSpy); + expect(bidRequestSpy.notCalled).to.be.true; + + const req1 = fakeServer.requests[0]; + const req2 = fakeServer.requests[1]; + const req3 = fakeServer.requests[2]; + + req1.respond(); + expect(bidRequestSpy.notCalled).to.be.true; + + req2.respond(); + expect(bidRequestSpy.notCalled).to.be.true; + + req3.respond(); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('executes callback after timeout', function () { + fetchTargetingInformation({ + mediaIDs: validMediaIDs + }); + jwplayerSubmodule.getData([], bidRequestSpy); + expect(bidRequestSpy.notCalled).to.be.true; + clock.tick(150); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('executes callback only once if requests succeed after timeout', function () { + fetchTargetingInformation({ + mediaIDs: validMediaIDs + }); + jwplayerSubmodule.getData([], bidRequestSpy); + expect(bidRequestSpy.notCalled).to.be.true; + clock.tick(150); + expect(bidRequestSpy.calledOnce).to.be.true; + + fakeServer.respond(); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('returns data in proper structure', function () { + const adUnitCode = 'test_ad_unit'; + const adUnitWithMediaId = { + code: adUnitCode, + jwTargeting: { + mediaID: testIdForSuccess + } + }; + const adUnitEmpty = { + code: 'test_ad_unit_empty' + }; + const expectedData = {}; + const expectedContentId = 'jw_' + testIdForSuccess; + expectedData[adUnitCode] = { + jwTargeting: { + segments: validSegments, + content: { + id: expectedContentId + } + } + }; + jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); + expect(bidRequestSpy.calledOnceWithExactly(expectedData)).to.be.true; + }); + + it('returns an empty object when media id is invalid', function () { + const adUnitCode = 'test_ad_unit'; + const adUnitWithMediaId = { + code: adUnitCode, + jwTargeting: { + mediaID: testIdForFailure + } + }; + const adUnitEmpty = { + code: 'test_ad_unit_empty' + }; + + jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); + expect(bidRequestSpy.calledOnceWithExactly({})).to.be.true; + }); + + it('returns an empty object when jwTargeting block is absent', function () { + const adUnitCode = 'test_ad_unit'; + const adUnitWithMediaId = { + code: adUnitCode, + mediaID: testIdForSuccess + }; + const adUnitEmpty = { + code: 'test_ad_unit_empty' + }; + + jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); + expect(bidRequestSpy.calledOnceWithExactly({})).to.be.true; + }); + }); + }); +}); diff --git a/test/spec/modules/kargoBidAdapter_spec.js b/test/spec/modules/kargoBidAdapter_spec.js index be73e140839..9dbcca8e331 100644 --- a/test/spec/modules/kargoBidAdapter_spec.js +++ b/test/spec/modules/kargoBidAdapter_spec.js @@ -505,7 +505,8 @@ describe('kargo adapter tests', function () { netRevenue: true, currency: 'USD', meta: { - clickUrl: 'https://foobar.com' + clickUrl: 'https://foobar.com', + advertiserDomains: ['https://foobar.com'] } }, { requestId: '3', diff --git a/test/spec/modules/mediagoBidAdapter_spec.js b/test/spec/modules/mediagoBidAdapter_spec.js new file mode 100644 index 00000000000..25cfc72672b --- /dev/null +++ b/test/spec/modules/mediagoBidAdapter_spec.js @@ -0,0 +1,101 @@ +import { + expect +} from 'chai'; +import { + spec +} from 'modules/mediagoBidAdapter.js'; + +describe('mediago:BidAdapterTests', function() { + let bidRequestData = { + 'bidderCode': 'mediago', + 'auctionId': '7fae02a9-0195-472f-ba94-708d3bc2c0d9', + 'bidderRequestId': '4fec04e87ad785', + 'bids': [{ + 'bidder': 'mediago', + 'params': { + 'token': '85a6b01e41ac36d49744fad726e3655d' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '5e24a2ce-db03-4565-a8a3-75dbddca9377', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '54d73f19c9d47a', + 'bidderRequestId': '4fec04e87ad785', + 'auctionId': '7fae02a9-0195-472f-ba94-708d3bc2c0d9', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }] + }; + let request = []; + + it('mediago:validate_pub_params', function() { + expect( + spec.isBidRequestValid({ + bidder: 'mediago', + params: { + token: ['85a6b01e41ac36d49744fad726e3655d'] + } + }) + ).to.equal(true); + }); + + it('mediago:validate_generated_params', function() { + request = spec.buildRequests(bidRequestData.bids, bidRequestData); + // console.log(request); + let req_data = JSON.parse(request.data); + expect(req_data.imp).to.have.lengthOf(1); + }); + + it('mediago:validate_response_params', function() { + let serverResponse = { + body: { + 'id': '7244645c-a81a-4760-8fd6-9d908d2c4a44', + 'seatbid': [{ + 'bid': [{ + 'id': 'aa86796a857ebedda9a2d7128a87dab1', + 'impid': '1', + 'price': 0.05, + 'nurl': 'http://d21uzv52i0cqie.cloudfront.net/api/winnotice?tn=341443089c0cb829164455a42d216ee3\u0026winloss=1\u0026id=aa86796a857ebedda9a2d7128a87dab1\u0026seat_id=${AUCTION_SEAT_ID}\u0026currency=${AUCTION_CURRENCY}\u0026bid_id=${AUCTION_BID_ID}\u0026ad_id=${AUCTION_AD_ID}\u0026loss=${AUCTION_LOSS}\u0026imp_id=1\u0026price=${AUCTION_PRICE}\u0026test=0\u0026time=1597714943\u0026adp=Dtnz0O4U8sdAU-XGGijCAgMbjDIeMGyCLXeSg1laXxM\u0026dsp_id=22\u0026url=-BFDu2NYtc4PYTplFW_2JcnDSRVLOOfaERbwJABjFyG6NUB4ywA3dUaXt5zPlyCUpBCOxjH9gk4E6yWTshzuSfQSx7g_TxvcXYUgh7YtY9NQZxx14InmNCTsezqID5UztV7llz8SXWHQ-ZsutH1nJIZzl1jH3i2uCPi91shqIZLN1bLJ5guAr5O4WyxVeOqIKyD_GiVcY9Olm51iI_3wgwFyDEN_dIDv-ObgNxpbPD0L11-62bjhGw3__7RuEo6XLdox-g46Fcqk6i0zayfsPM4QeMAhWJ4lsg-xswSI0YAfzyoOIeTWB78mdpt_GmN5PKZZPqyO7VkbwHEasn-mTyYTddbz5v2fzEkRO0AQZtAZx96PANGrNvcOHnRVmCdkzN96b5Ur1_8ipdyzHOFRtJ-z_KmKaxig6himvMCePozZvrvihiGhigP4RGiFT7ytVYKHyUGAV2PF5SwtgnB0uGCltd7o1CLhZyZEQNgE7LSESyGztZ5kM9N_VZV9gPZVhvlJDfYFNRW9i6D2pZxV0Gd63rA9gpeUJ3mhbkj-B27VRKrNTBSrwIAU7P0RPD5_opl3G8nPD1Ce2vKuQK8qynHWQblfeA61nDok-fRezSKbzwepqi8oxXadFrCmN7KxP_mPqA794xYzIw5-mS64NA', + 'burl': 'http://d21uzv52i0cqie.cloudfront.net/api/winnotice?tn=341443089c0cb829164455a42d216ee3\u0026winloss=2\u0026id=aa86796a857ebedda9a2d7128a87dab1\u0026seat_id=${AUCTION_SEAT_ID}\u0026currency=${AUCTION_CURRENCY}\u0026bid_id=${AUCTION_BID_ID}\u0026ad_id=${AUCTION_AD_ID}\u0026loss=${AUCTION_LOSS}\u0026imp_id=1\u0026price=${AUCTION_PRICE}\u0026test=0\u0026time=1597714943\u0026adp=Dtnz0O4U8sdAU-XGGijCAgMbjDIeMGyCLXeSg1laXxM\u0026dsp_id=22\u0026url=dXerAvyp4zYQzsQ56eGB4JtiA4yFaYlTqcHffccrvCg', + 'lurl': 'http://d21uzv52i0cqie.cloudfront.net/api/winnotice?tn=341443089c0cb829164455a42d216ee3\u0026winloss=0\u0026id=aa86796a857ebedda9a2d7128a87dab1\u0026seat_id=${AUCTION_SEAT_ID}\u0026currency=${AUCTION_CURRENCY}\u0026bid_id=${AUCTION_BID_ID}\u0026ad_id=${AUCTION_AD_ID}\u0026loss=${AUCTION_LOSS}\u0026imp_id=1\u0026price=${AUCTION_PRICE}\u0026test=0\u0026time=1597714943\u0026adp=Dtnz0O4U8sdAU-XGGijCAgMbjDIeMGyCLXeSg1laXxM\u0026dsp_id=22\u0026url=ptSxg_vR7-fdx-WAkkUADJb__BntE5a6-RSeYdUewvk', + 'adm': '\u003clink rel=\"stylesheet\" href=\"//cdn.mediago.io/js/style/style_banner_300*250.css\"\u003e\u003cdiv id=\"mgcontainer-583ce3286b442001205b2fb9a5488efc\" class=\"mediago-placement imgTopTitleBottom\" style=\"position:relative;width:298px;height:248px;overflow:hidden\"\u003e\u003ca href=\"http://trace.mediago.io/api/bidder/track?tn=341443089c0cb829164455a42d216ee3\u0026price=PRMC8pCHtH55ipgXubUs8jlsKTBxWRSEweH8Mee_aUQ\u0026evt=102\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026campaignid=1003540\u0026impid=25-300x175-1\u0026offerid=1107782\u0026test=0\u0026time=1597714943\u0026cp=GJA03gjm-ugPTmN7prOpvhfu1aA04IgpTcW4oRX22Lg\u0026clickid=25_aa86796a857ebedda9a2d7128a87dab1_25-300x175-1\u0026acid=164\u0026trackingid=583ce3286b442001205b2fb9a5488efc\u0026uid=6dda6c6b70eb4e2d9ab3469d921f2c74\u0026jt=2\u0026url=PQFFci337KgCVkx7KTgRItClLaWH0lgTcIlgBRTpfKngVJES4uKLfxXz9mjLcDWIbWQcEVVk_gfTcIaK8oKO2YyVG5lc3hjZeZr0VaIDHbWggPJaqtfDK9T0HZIKvrpe\" target=\"_blank\"\u003e\u003cimg alt=\"Robert Vowed To Keep Silent, But Decided To Put The People First And Speak\" src=\"https://d2cli4kgl5uxre.cloudfront.net/ML/b5e361889beef5eaf69987384b7a56e8__300x175.png\" style=\"height:70%;width:100%;border-width:0;border:none;\"\u003e\u003ch3 class=\"title\" style=\"font-size:16px;\"\u003eRobert Vowed To Keep Silent, But Decided To Put The People First And Speak\u003c/h3\u003e\u003c/a\u003e\u003cspan class=\"source\"\u003e\u003ca class=\"sourcename\" href=\"//www.mediago.io\" target=\"_blank\"\u003e\u003cspan\u003eAd\u003c/span\u003e \u003c/a\u003e\u003ca class=\"srcnameadslabelurl\" href=\"//www.mediago.io/privacy\" target=\"_blank\"\u003e\u003cspan\u003eViral Net News\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e\u003c/div\u003e\u003cscript\u003e!function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){\"undefined\"!=typeof Symbol\u0026\u0026Symbol.toStringTag\u0026\u0026Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},n.t=function(e,t){if(1\u0026t\u0026\u0026(e=n(e)),8\u0026t)return e;if(4\u0026t\u0026\u0026\"object\"==typeof e\u0026\u0026e\u0026\u0026e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,\"default\",{enumerable:!0,value:e}),2\u0026t\u0026\u0026\"string\"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e\u0026\u0026e.__esModule?function(){return e.default}:function(){return e};return n.d(t,\"a\",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p=\"\",n(n.s=24)}({24:function(e,t,n){\"use strict\";function o(e){var t=new Image;t.src=e,t.style=\"display:none;visibility:hidden\",t.width=0,t.height=0,document.body.appendChild(t)}o(\"http://d21uzv52i0cqie.cloudfront.net/api/bidder/track?tn=341443089c0cb829164455a42d216ee3\u0026price=PRMC8pCHtH55ipgXubUs8jlsKTBxWRSEweH8Mee_aUQ\u0026evt=101\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026campaignid=1003540\u0026impid=25-300x175-1\u0026offerid=1107782\u0026test=0\u0026time=1597714943\u0026cp=GJA03gjm-ugPTmN7prOpvhfu1aA04IgpTcW4oRX22Lg\u0026acid=164\u0026trackingid=583ce3286b442001205b2fb9a5488efc\u0026uid=6dda6c6b70eb4e2d9ab3469d921f2c74\");var r=document.getElementById(\"mgcontainer-583ce3286b442001205b2fb9a5488efc\"),i=!1;!function e(){setTimeout((function(){var t,n;!i\u0026\u0026(t=r,n=window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight,(t.getBoundingClientRect()\u0026\u0026t.getBoundingClientRect().top)\u003c=n-.75*(t.offsetHeight||t.clientHeight))?(i=!0,o(\"http://d21uzv52i0cqie.cloudfront.net/api/bidder/track?tn=341443089c0cb829164455a42d216ee3\u0026price=PRMC8pCHtH55ipgXubUs8jlsKTBxWRSEweH8Mee_aUQ\u0026evt=104\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026campaignid=1003540\u0026impid=25-300x175-1\u0026offerid=1107782\u0026test=0\u0026time=1597714943\u0026cp=GJA03gjm-ugPTmN7prOpvhfu1aA04IgpTcW4oRX22Lg\u0026acid=164\u0026trackingid=583ce3286b442001205b2fb9a5488efc\u0026uid=6dda6c6b70eb4e2d9ab3469d921f2c74\u0026sid=16__11__13\u0026format=\u0026crid=b5e361889beef5eaf69987384b7a56e8\")):e()}),500)}()}});\u003c/script\u003e\u003cscript type=\"text/javascript\" src=\"http://d21uzv52i0cqie.cloudfront.net/api/track?tn=341443089c0cb829164455a42d216ee3\u0026price=${AUCTION_PRICE}\u0026evt=5\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026impid=1\u0026offerid=\u0026tagid=\u0026test=0\u0026time=1597714943\u0026adp=5zrCZ2rC-WLafYkNpeTnzA72tDqVZUlOA3Js6_eJjYU\u0026dsp_id=22\u0026cp=${cp}\u0026url=\u0026type=script\"\u003e\u003c/script\u003e\u003cscript\u003edocument.addEventListener\u0026\u0026document.addEventListener(\"click\",function(){var a=document.createElement(\"script\");a.src=\"http://d21uzv52i0cqie.cloudfront.net/api/track?tn=341443089c0cb829164455a42d216ee3\u0026price=${AUCTION_PRICE}\u0026evt=6\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026impid=1\u0026offerid=\u0026tagid=\u0026test=0\u0026time=1597714943\u0026adp=5zrCZ2rC-WLafYkNpeTnzA72tDqVZUlOA3Js6_eJjYU\u0026dsp_id=22\u0026cp=${cp}\u0026url=\u0026clickid=25_aa86796a857ebedda9a2d7128a87dab1_1\";document.body.appendChild(a)});\u003c/script\u003e', + 'cid': '1003540', + 'crid': 'b5e361889beef5eaf69987384b7a56e8', + 'w': 300, + 'h': 250 + }] + }], + 'cur': 'USD' + } + }; + + let bids = spec.interpretResponse(serverResponse); + // console.log({ + // bids + // }); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + + expect(bid.creativeId).to.equal('b5e361889beef5eaf69987384b7a56e8'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.currency).to.equal('USD'); + }); +}); diff --git a/test/spec/modules/oneVideoBidAdapter_spec.js b/test/spec/modules/oneVideoBidAdapter_spec.js index a91e9ac27e8..ae29bcd48ec 100644 --- a/test/spec/modules/oneVideoBidAdapter_spec.js +++ b/test/spec/modules/oneVideoBidAdapter_spec.js @@ -217,7 +217,7 @@ describe('OneVideoBidAdapter', function () { const placement = bidRequest.params.video.placement; const rewarded = bidRequest.params.video.rewarded; const inventoryid = bidRequest.params.video.inventoryid; - const VERSION = '3.0.3'; + const VERSION = '3.0.4'; expect(data.imp[0].video.w).to.equal(width); expect(data.imp[0].video.h).to.equal(height); expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); @@ -593,14 +593,25 @@ describe('OneVideoBidAdapter', function () { it('should get correct user sync when iframeEnabled', function () { let pixel = spec.getUserSyncs({pixelEnabled: true}, {}, {gdprApplies: true, consentString: GDPR_CONSENT_STRING}) - expect(pixel[2].type).to.equal('image'); - expect(pixel[2].url).to.equal('https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=1&gdpr_consent=' + GDPR_CONSENT_STRING + '&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0&gdpr=1&gdpr_consent=' + encodeURI(GDPR_CONSENT_STRING)); + expect(pixel[1].type).to.equal('image'); + expect(pixel[1].url).to.equal('https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=1&gdpr_consent=' + GDPR_CONSENT_STRING + '&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0&gdpr=1&gdpr_consent=' + encodeURI(GDPR_CONSENT_STRING)); }); it('should default to gdprApplies=0 when consentData is undefined', function () { let pixel = spec.getUserSyncs({pixelEnabled: true}, {}, undefined); - expect(pixel[2].url).to.equal('https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=0&gdpr_consent=&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0&gdpr=0&gdpr_consent='); + expect(pixel[1].url).to.equal('https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=0&gdpr_consent=&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0&gdpr=0&gdpr_consent='); }); }); + + describe('verify sync pixels', function () { + let pixel = spec.getUserSyncs({pixelEnabled: true}, {}, undefined); + it('should be UPS sync pixel for DBM', function () { + expect(pixel[0].url).to.equal('https://pixel.advertising.com/ups/57304/sync?gdpr=&gdpr_consent=&_origin=0&redir=true') + }); + + it('should be TTD sync pixel', function () { + expect(pixel[2].url).to.equal('https://match.adsrvr.org/track/cmf/generic?ttd_pid=adaptv&ttd_tpi=1') + }); + }) }); }); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index b75ede0e9db..5abe068c100 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -547,6 +547,37 @@ describe('S2S Adapter', function () { expect(requestBid.user).to.not.exist; }); + it('adds additional consent information to ortb2 request depending on presence of module', function () { + let ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: ortb2Config }; + config.setConfig(consentConfig); + + let gdprBidRequest = utils.deepClone(BID_REQUESTS); + gdprBidRequest[0].gdprConsent = { + consentString: 'abc123', + addtlConsent: 'superduperconsent', + gdprApplies: true + }; + + adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(server.requests[0].requestBody); + + expect(requestBid.regs.ext.gdpr).is.equal(1); + expect(requestBid.user.ext.consent).is.equal('abc123'); + expect(requestBid.user.ext.ConsentedProvidersSettings.consented_providers).is.equal('superduperconsent'); + + config.resetConfig(); + config.setConfig({ s2sConfig: CONFIG }); + + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + requestBid = JSON.parse(server.requests[1].requestBody); + + 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 = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index 0a006e0eb48..d4ac2e5ad72 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -1101,6 +1101,39 @@ describe('the price floors module', function () { floor: 1.3334 // 1.3334 * 0.75 = 1.000005 which is the floor (we cut off getFloor at 4 decimal points) }); }); + it('should work when cpmAdjust function uses bid object', function () { + getGlobal().bidderSettings = { + rubicon: { + bidCpmAdjustment: function (bidCpm, bidResponse) { + return bidResponse.cpm * 0.5; + }, + }, + appnexus: { + bidCpmAdjustment: function (bidCpm, bidResponse) { + return bidResponse.cpm * 0.75; + }, + } + }; + _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 }; + let appnexusBid = { + ...bidRequest, + bidder: 'appnexus' + }; + + // the conversion should be what the bidder would need to return in order to match the actual floor + // rubicon + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 2.0 // a 2.0 bid after rubicons cpm adjustment would be 1.0 and thus is the floor after adjust + }); + + // appnexus + expect(appnexusBid.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.3334 // 1.3334 * 0.75 = 1.000005 which is the floor (we cut off getFloor at 4 decimal points) + }); + }); it('should correctly pick the right attributes if * is passed in and context can be assumed', function () { let inputBidReq = { bidder: 'rubicon', diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index dd46646abc8..0f51a8df61c 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -26,6 +26,9 @@ describe('PubMatic adapter', function () { let bannerBidResponse; let videoBidResponse; let schainConfig; + let outstreamBidRequest; + let validOutstreamBidRequest; + let outstreamVideoBidResponse; beforeEach(function () { schainConfig = { @@ -55,7 +58,7 @@ describe('PubMatic adapter', function () { } }, params: { - publisherId: '301', + publisherId: '5670', adSlot: '/15671365/DMDemo@300x250:0', kadfloor: '1.2', pmzoneid: 'aabc, ddef', @@ -656,7 +659,89 @@ describe('PubMatic adapter', function () { }] }] } - } + }; + outstreamBidRequest = + [ + { + code: 'video1', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'outstream' + } + }, + bidder: 'pubmatic', + bidId: '47acc48ad47af5', + requestId: '0fb4905b-1234-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + params: { + publisherId: '5670', + outstreamAU: 'pubmatic-test', + adSlot: 'Div1@0x0', // ad_id or tagid + video: { + mimes: ['video/mp4', 'video/x-flv'], + skippable: true, + minduration: 5, + maxduration: 30 + } + } + } + ]; + + validOutstreamBidRequest = { + auctionId: '92489f71-1bf2-49a0-adf9-000cea934729', + auctionStart: 1585918458868, + bidderCode: 'pubmatic', + bidderRequestId: '47acc48ad47af5', + bids: [{ + adUnitCode: 'video1', + auctionId: '92489f71-1bf2-49a0-adf9-000cea934729', + bidId: '47acc48ad47af5', + bidRequestsCount: 1, + bidder: 'pubmatic', + bidderRequestId: '47acc48ad47af5', + mediaTypes: { + video: { + context: 'outstream' + } + }, + params: { + publisherId: '5670', + outstreamAU: 'pubmatic-test', + adSlot: 'Div1@0x0', // ad_id or tagid + video: { + mimes: ['video/mp4', 'video/x-flv'], + skippable: true, + minduration: 5, + maxduration: 30 + } + }, + sizes: [[768, 432], [640, 480], [630, 360]], + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' + }], + start: 11585918458869, + timeout: 3000 + }; + + outstreamVideoBidResponse = { + 'body': { + 'id': '93D3BAD6-E2E2-49FB-9D89-920B1761C865', + 'seatbid': [{ + 'bid': [{ + 'id': '0fb4905b-1234-4152-86be-c6f6d259ba99', + 'impid': '47acc48ad47af5', + 'price': 1.3, + 'adm': 'Acudeo CompatibleVAST 2.0 Instream Test 1VAST 2.0 Instream Test 1https://dsptracker.com/{PSPM}00:00:04https://www.pubmatic.com', + 'h': 250, + 'w': 300, + 'ext': { + 'deal_channel': 6 + } + }] + }] + } + }; }); describe('implementation', function () { @@ -725,7 +810,17 @@ describe('PubMatic adapter', function () { let request = spec.buildRequests(bidRequests); expect(request.url).to.equal('https://hbopenbid.pubmatic.com/translator?source=prebid-client'); expect(request.method).to.equal('POST'); - }); + }); + + it('should return bidderRequest property', function() { + let request = spec.buildRequests(bidRequests, validOutstreamBidRequest); + expect(request.bidderRequest).to.equal(validOutstreamBidRequest); + }); + + it('bidderRequest should be undefined if bidderRequest is not present', function() { + let request = spec.buildRequests(bidRequests); + expect(request.bidderRequest).to.be.undefined; + }); it('test flag not sent when pubmaticTest=true is absent in page url', function() { let request = spec.buildRequests(bidRequests); @@ -776,6 +871,23 @@ describe('PubMatic adapter', function () { expect(data.source.ext.schain).to.deep.equal(bidRequests[0].schain); }); + it('Set content from config, set site.content', function() { + let sandbox = sinon.sandbox.create(); + const content = { + 'id': 'alpha-numeric-id' + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + content: content + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.site.content).to.deep.equal(content); + sandbox.restore(); + }); + it('Merge the device info from config', function() { let sandbox = sinon.sandbox.create(); sandbox.stub(config, 'getConfig').callsFake((key) => { @@ -840,6 +952,62 @@ describe('PubMatic adapter', function () { sandbox.restore(); }); + it('Set app, content from config, copy publisher and ext from site, unset site, config.content in app.content', function() { + let sandbox = sinon.sandbox.create(); + const content = { + 'id': 'alpha-numeric-id' + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + content: content, + app: { + bundle: 'org.prebid.mobile.demoapp', + domain: 'prebid.org' + } + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); + expect(data.app.domain).to.equal('prebid.org'); + expect(data.app.publisher.id).to.equal(bidRequests[0].params.publisherId); + expect(data.app.ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); + expect(data.app.content).to.deep.equal(content); + expect(data.site).to.not.exist; + sandbox.restore(); + }); + + it('Set app.content, content from config, copy publisher and ext from site, unset site, config.app.content in app.content', function() { + let sandbox = sinon.sandbox.create(); + const content = { + 'id': 'alpha-numeric-id' + }; + const appContent = { + id: 'app-content-id-2' + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + content: content, + app: { + bundle: 'org.prebid.mobile.demoapp', + domain: 'prebid.org', + content: appContent + } + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); + expect(data.app.domain).to.equal('prebid.org'); + expect(data.app.publisher.id).to.equal(bidRequests[0].params.publisherId); + expect(data.app.ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); + expect(data.app.content).to.deep.equal(appContent); + expect(data.site).to.not.exist; + sandbox.restore(); + }); + it('Request params check: without adSlot', function () { delete bidRequests[0].params.adSlot; let request = spec.buildRequests(bidRequests); @@ -2381,7 +2549,6 @@ describe('PubMatic adapter', function () { it('should check for valid video mediaType in case of multiformat request', function() { let request = spec.buildRequests(videoBidRequests); let response = spec.interpretResponse(videoBidResponse, request); - expect(response[0].mediaType).to.equal('video'); }); @@ -2391,6 +2558,36 @@ describe('PubMatic adapter', function () { expect(response[0].mediaType).to.equal('native'); }); + + it('should assign renderer if bid is video and request is for outstream', function() { + let request = spec.buildRequests(outstreamBidRequest, validOutstreamBidRequest); + let response = spec.interpretResponse(outstreamVideoBidResponse, request); + expect(response[0].renderer).to.exist; + }); + + it('should not assign renderer if bidderRequest is not present', function() { + let request = spec.buildRequests(outstreamBidRequest); + let response = spec.interpretResponse(outstreamVideoBidResponse, request); + expect(response[0].renderer).to.not.exist; + }); + + it('should not assign renderer if bid is video and request is for instream', function() { + let request = spec.buildRequests(videoBidRequests); + let response = spec.interpretResponse(videoBidResponse, request); + expect(response[0].renderer).to.not.exist; + }); + + it('should not assign renderer if bid is native', function() { + let request = spec.buildRequests(nativeBidRequests); + let response = spec.interpretResponse(nativeBidResponse, request); + expect(response[0].renderer).to.not.exist; + }); + + it('should not assign renderer if bid is of banner', function() { + let request = spec.buildRequests(bidRequests); + let response = spec.interpretResponse(bidResponses, request); + expect(response[0].renderer).to.not.exist; + }); }); describe('getUserSyncs', function() { diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index cd168ec61e6..caa554c8cd8 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -430,26 +430,6 @@ describe('Quantcast adapter', function () { expect(requests).to.equal(undefined); }); - it('allows TCF v2 request from Germany for purpose 1', function () { - const bidderRequest = { - gdprConsent: { - gdprApplies: true, - consentString: 'consentString', - vendorData: { - publisherCC: 'DE', - purposeOneTreatment: true - }, - apiVersion: 2 - } - }; - - const requests = qcSpec.buildRequests([bidRequest], bidderRequest); - const parsed = JSON.parse(requests[0].data); - - expect(parsed.gdprSignal).to.equal(1); - expect(parsed.gdprConsent).to.equal('consentString'); - }); - it('allows TCF v2 request when Quantcast has consent for purpose 1', function() { const bidderRequest = { gdprConsent: { diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index ca149fe7a44..f47068724d1 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -1,27 +1,176 @@ -import { - init, - requestBidsHook, - setTargetsAfterRequestBids, - deepMerge, - validateProviderDataForGPT -} from 'modules/rtdModule/index.js'; -import { - init as browsiInit, - addBrowsiTag, - isIdMatchingAdUnit, - setData, - getMacroId -} from 'modules/browsiRtdProvider.js'; -import { - init as audigentInit, - setData as setAudigentData -} from 'modules/audigentRtdProvider.js'; +import * as rtdModule from 'modules/rtdModule/index.js'; import { config } from 'src/config.js'; -import { makeSlot } from '../integration/faker/googletag.js'; - -let expect = require('chai').expect; +import {makeSlot} from '../integration/faker/googletag.js'; +import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; + +const validSM = { + name: 'validSM', + init: () => { return true }, + getData: (adUnits, onDone) => { + setTimeout(() => { + return onDone({'key': 'validSM'}) + }, 500) + } +}; + +const validSMWait = { + name: 'validSMWait', + init: () => { return true }, + getData: (adUnits, onDone) => { + setTimeout(() => { + return onDone({'ad1': {'key': 'validSMWait'}}) + }, 50) + } +}; + +const invalidSM = { + name: 'invalidSM' +}; + +const failureSM = { + name: 'failureSM', + init: () => { return false } +}; + +const nonConfSM = { + name: 'nonConfSM', + init: () => { return true } +}; + +const conf = { + 'realTimeData': { + 'auctionDelay': 250, + dataProviders: [ + { + 'name': 'validSMWait', + 'waitForIt': true, + }, + { + 'name': 'validSM', + 'waitForIt': false, + }, + { + 'name': 'invalidSM' + }, + { + 'name': 'failureSM' + }] + } +}; + +function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: { banner: {}, native: {} }, + sizes: [[300, 200], [300, 600]], + bids: [{ bidder: 'sampleBidder', params: { placementId: 'banner-only-bidder' } }] + }; +} describe('Real time module', function () { + after(function () { + config.resetConfig(); + }); + + beforeEach(function () { + config.setConfig(conf); + }); + + it('should use only valid modules', function (done) { + rtdModule.attachRealTimeDataProvider(validSM); + rtdModule.attachRealTimeDataProvider(invalidSM); + rtdModule.attachRealTimeDataProvider(failureSM); + rtdModule.attachRealTimeDataProvider(nonConfSM); + rtdModule.attachRealTimeDataProvider(validSMWait); + rtdModule.initSubModules(afterInitSubModules); + function afterInitSubModules() { + expect(rtdModule.subModules).to.eql([validSMWait, validSM]); + done(); + } + rtdModule.init(config); + }); + + it('should only wait for must have sub modules', function (done) { + rtdModule.getProviderData([], (data) => { + expect(data).to.eql({validSMWait: {'ad1': {'key': 'validSMWait'}}}); + done(); + }) + }); + + it('deep merge object', function () { + const obj1 = { + id1: { + key: 'value', + key2: 'value2' + }, + id2: { + k: 'v' + } + }; + const obj2 = { + id1: { + key3: 'value3' + } + }; + const obj3 = { + id3: { + key: 'value' + } + }; + const expected = { + id1: { + key: 'value', + key2: 'value2', + key3: 'value3' + }, + id2: { + k: 'v' + }, + id3: { + key: 'value' + } + }; + + const merged = rtdModule.deepMerge([obj1, obj2, obj3]); + assert.deepEqual(expected, merged); + }); + + it('check module using bidsBackCallback', function (done) { + // set slot + const slot = makeSlot({ code: '/code1', divId: 'ad1' }); + window.googletag.pubads().setSlots([slot]); + + function afterBidHook() { + expect(slot.getTargeting().length).to.equal(1); + expect(slot.getTargeting()[0].key).to.equal('validSMWait'); + done(); + } + rtdModule.setTargetsAfterRequestBids(afterBidHook, []); + }); + + it('check module using requestBidsHook', function (done) { + // set slot + const slotsB = makeSlot({ code: '/code1', divId: 'ad1' }); + window.googletag.pubads().setSlots([slotsB]); + let adUnits = [getAdUnitMock('ad1')]; + + function afterBidHook(data) { + expect(slotsB.getTargeting().length).to.equal(1); + expect(slotsB.getTargeting()[0].key).to.equal('validSMWait'); + + data.adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid.realTimeData).to.have.property('key'); + expect(bid.realTimeData.key).to.equal('validSMWait'); + }); + }); + done(); + } + rtdModule.requestBidsHook(afterBidHook, { adUnits: adUnits }); + }); +}); + +describe('browsi Real time data sub module', function () { const conf = { 'realTimeData': { 'auctionDelay': 250, @@ -33,250 +182,93 @@ describe('Real time module', function () { 'pubKey': 'testPub', 'keyName': 'bv' } - }, { - 'name': 'audigent' }] } }; - const predictions = { - p: { - 'browsiAd_2': { - 'w': [ - '/57778053/Browsi_Demo_Low', - '/57778053/Browsi_Demo_300x250' - ], - 'p': 0.07 - }, - 'browsiAd_1': { - 'w': [], - 'p': 0.06 - }, - 'browsiAd_3': { - 'w': [], - 'p': 0.53 - }, - 'browsiAd_4': { - 'w': [ - '/57778053/Browsi_Demo' - ], - 'p': 0.85 - } - } - }; - - const audigentSegments = { - audigent_segments: { 'a': 1, 'b': 2 } - } - - function getAdUnitMock(code = 'adUnit-code') { - return { - code, - mediaTypes: { banner: {}, native: {} }, - sizes: [[300, 200], [300, 600]], - bids: [{ bidder: 'sampleBidder', params: { placementId: 'banner-only-bidder' } }] - }; - } - - function createSlots() { - const slot1 = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - const slot2 = makeSlot({ code: '/57778053/Browsi', divId: 'browsiAd_1' }); - return [slot1, slot2]; - } - - describe('Real time module with browsi provider', function () { - afterEach(function () { - $$PREBID_GLOBAL$$.requestBids.removeAll(); - }); + beforeEach(function () { + config.setConfig(conf); + }); - after(function () { - config.resetConfig(); - }); + after(function () { + config.resetConfig(); + }); - it('check module using bidsBackCallback', function () { - let adUnits1 = [getAdUnitMock('browsiAd_1')]; - let targeting = []; - init(config); - browsiInit(config); - config.setConfig(conf); - setData(predictions); - - // set slot - const slots = createSlots(); - window.googletag.pubads().setSlots(slots); - - function afterBidHook() { - slots.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); + it('should init and return true', function () { + browsiRTD.beforeInit(config); + expect(browsiRTD.browsiSubmodule.init()).to.equal(true) + }); - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); - } - setTargetsAfterRequestBids(afterBidHook, adUnits1, true); - }); + it('should create browsi script', function () { + const script = browsiRTD.addBrowsiTag('scriptUrl.com'); + expect(script.getAttribute('data-sitekey')).to.equal('testKey'); + expect(script.getAttribute('data-pubkey')).to.equal('testPub'); + expect(script.async).to.equal(true); + expect(script.prebidData.kn).to.equal(conf.realTimeData.dataProviders[0].params.keyName); + }); - it('check module using requestBidsHook', function () { - let adUnits1 = [getAdUnitMock('browsiAd_1')]; - let targeting = []; - let dataReceived = null; - - // set slot - const slotsB = createSlots(); - window.googletag.pubads().setSlots(slotsB); - - function afterBidHook(data) { - dataReceived = data; - slotsB.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); + it('should match placement with ad unit', function () { + const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); - dataReceived.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('bv'); - }); - }); - } - requestBidsHook(afterBidHook, { adUnits: adUnits1 }); - }); + const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250']); // true + const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true + const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_Low']); // false + const test4 = browsiRTD.isIdMatchingAdUnit(slot, []); // true - it('check object deep merge', function () { - const obj1 = { - id1: { - key: 'value', - key2: 'value2' - }, - id2: { - k: 'v' - } - }; - const obj2 = { - id1: { - key3: 'value3' - } - }; - const obj3 = { - id3: { - key: 'value' - } - }; - const expected = { - id1: { - key: 'value', - key2: 'value2', - key3: 'value3' - }, - id2: { - k: 'v' - }, - id3: { - key: 'value' - } - }; + expect(test1).to.equal(true); + expect(test2).to.equal(true); + expect(test3).to.equal(false); + expect(test4).to.equal(true); + }); - const merged = deepMerge([obj1, obj2, obj3]); - assert.deepEqual(expected, merged); - }); + it('should return correct macro values', function () { + const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - it('check data validation for GPT targeting', function () { - // non strings values should be removed - const obj = { - valid: {'key': 'value'}, - invalid: {'key': ['value']}, - combine: { - 'a': 'value', - 'b': [] - } - }; + slot.setTargeting('test', ['test', 'value']); + // slot getTargeting doesn't act like GPT so we can't expect real value + const macroResult = browsiRTD.getMacroId({p: '/'}, slot); + expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); - const expected = { - valid: {'key': 'value'}, - invalid: {}, - combine: { - 'a': 'value', - } - }; - const validationResult = validateProviderDataForGPT(obj); - assert.deepEqual(expected, validationResult); - }); + const macroResultB = browsiRTD.getMacroId({}, slot); + expect(macroResultB).to.equal('browsiAd_1'); - it('check browsi sub module', function () { - const script = addBrowsiTag('scriptUrl.com'); - expect(script.getAttribute('data-sitekey')).to.equal('testKey'); - expect(script.getAttribute('data-pubkey')).to.equal('testPub'); - expect(script.async).to.equal(true); - - const slots = createSlots(); - const test1 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_300x250']); // true - const test2 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true - const test3 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_Low']); // false - const test4 = isIdMatchingAdUnit(slots[0], []); // true - - expect(test1).to.equal(true); - expect(test2).to.equal(true); - expect(test3).to.equal(false); - expect(test4).to.equal(true); - - // macro results - slots[0].setTargeting('test', ['test', 'value']); - // slot getTargeting doesn't act like GPT so we can't expect real value - const macroResult = getMacroId({p: '/'}, slots[0]); - expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); - - const macroResultB = getMacroId({}, slots[0]); - expect(macroResultB).to.equal('browsiAd_1'); - - const macroResultC = getMacroId({p: '', s: {s: 0, e: 1}}, slots[0]); - expect(macroResultC).to.equal('/'); - }) + const macroResultC = browsiRTD.getMacroId({p: '', s: {s: 0, e: 1}}, slot); + expect(macroResultC).to.equal('/'); }); - describe('Real time module with Audigent provider', function () { - before(function () { - init(config); - audigentInit(config); - config.setConfig(conf); - setAudigentData(audigentSegments); + describe('should return data to RTD module', function () { + it('should return empty if no ad units defined', function (done) { + browsiRTD.setData({}); + browsiRTD.browsiSubmodule.getData([], onDone); + function onDone(data) { + expect(data).to.eql({}); + done(); + } }); - afterEach(function () { - $$PREBID_GLOBAL$$.requestBids.removeAll(); - config.resetConfig(); + it('should return NA if no prediction for ad unit', function (done) { + const adUnits = [getAdUnitMock('adMock')]; + browsiRTD.setData({}); + browsiRTD.browsiSubmodule.getData(adUnits, onDone); + function onDone(data) { + expect(data).to.eql({adMock: {bv: 'NA'}}); + done(); + } }); - it('check module using requestBidsHook', function () { - let adUnits1 = [getAdUnitMock('audigentAd_1')]; - let targeting = []; - let dataReceived = null; - - // set slot - const slotsB = createSlots(); - window.googletag.pubads().setSlots(slotsB); - - function afterBidHook(data) { - dataReceived = data; - slotsB.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); - - dataReceived.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('audigent_segments'); - expect(bid.realTimeData.audigent_segments).to.deep.equal(audigentSegments.audigent_segments); - }); - }); + it('should return prediction from server', function (done) { + const adUnits = [getAdUnitMock('hasPrediction')]; + const data = { + p: {'hasPrediction': {p: 0.234}}, + kn: 'bv', + pmd: undefined + }; + browsiRTD.setData(data); + browsiRTD.browsiSubmodule.getData(adUnits, onDone); + function onDone(data) { + expect(data).to.eql({hasPrediction: {bv: '0.20'}}); + done(); } - - requestBidsHook(afterBidHook, { adUnits: adUnits1 }); - }); - }); + }) + }) }); diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 466435d9652..0c2c83a4b37 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -743,7 +743,8 @@ describe('rubicon analytics adapter', function () { enforcement: true, dealsEnforced: false, skipRate: 15, - fetchStatus: 'error' + fetchStatus: 'error', + provider: 'rubicon' }); // first adUnit's adSlot expect(message.auctions[0].adUnits[0].adSlot).to.equal('12345/sports'); @@ -765,19 +766,28 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); }); - it('should not send floor info if provider is not rubicon', function () { + it('should still send floor info if provider is not rubicon', function () { let message = performFloorAuction('randomProvider') // verify our floor stuff is passed // top level floor info - expect(message.auctions[0].floors).to.be.undefined; + expect(message.auctions[0].floors).to.deep.equal({ + location: 'setConfig', + modelName: 'someModelName', + skipped: false, + enforcement: true, + dealsEnforced: false, + skipRate: 15, + fetchStatus: 'error', + provider: 'randomProvider' + }); // first adUnit's adSlot expect(message.auctions[0].adUnits[0].adSlot).to.equal('12345/sports'); // since no other bids, we set adUnit status to no-bid expect(message.auctions[0].adUnits[0].status).to.equal('no-bid'); // first adUnits bid is rejected expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected'); - expect(message.auctions[0].adUnits[0].bids[0].bidResponse.floorValue).to.be.undefined; + expect(message.auctions[0].adUnits[0].bids[0].bidResponse.floorValue).to.equal(4); // if bid rejected should take cpmAfterAdjustments val expect(message.auctions[0].adUnits[0].bids[0].bidResponse.bidPriceUSD).to.equal(2.1); @@ -787,7 +797,7 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].status).to.equal('success'); // second adUnits bid is success expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); - expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.be.undefined; + expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); }); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index c21a9b879f1..c28af4a3f9b 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1,10 +1,10 @@ import {expect} from 'chai'; -import adapterManager from 'src/adapterManager.js'; import {spec, getPriceGranularity, masSizeOrdering, resetUserSync, hasVideoMediaType, FASTLANE_ENDPOINT} from 'modules/rubiconBidAdapter.js'; import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import find from 'core-js-pure/features/array/find.js'; +import { createEidsArray } from 'modules/userId/eids.js'; const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be substituted in by gulp in built prebid const PBS_INTEGRATION = 'pbjs'; @@ -13,7 +13,8 @@ describe('the rubicon adapter', function () { let sandbox, bidderRequest, sizeMap, - getFloorResponse; + getFloorResponse, + logErrorSpy; /** * @typedef {Object} sizeMapConverted @@ -219,12 +220,13 @@ describe('the rubicon adapter', function () { 'size_id': 201, }; bid.userId = { - lipb: { - lipbid: '0000-1111-2222-3333', - segments: ['segA', 'segB'] - }, - idl_env: '1111-2222-3333-4444' + lipb: { lipbid: '0000-1111-2222-3333', segments: ['segA', 'segB'] }, + idl_env: '1111-2222-3333-4444', + sharedid: { id: '1111', third: '2222' }, + tdid: '3000', + pubcid: '4000' }; + bid.userIdAsEids = createEidsArray(bid.userId); bid.storedAuctionResponse = 11111; } @@ -272,6 +274,7 @@ describe('the rubicon adapter', function () { beforeEach(function () { sandbox = sinon.sandbox.create(); + logErrorSpy = sinon.spy(utils, 'logError'); getFloorResponse = {}; bidderRequest = { bidderCode: 'rubicon', @@ -343,6 +346,7 @@ describe('the rubicon adapter', function () { afterEach(function () { sandbox.restore(); + utils.logError.restore(); }); describe('MAS mapping / ordering', function () { @@ -412,7 +416,18 @@ describe('the rubicon adapter', function () { it('should correctly send hard floors when getFloor function is present and returns valid floor', function () { // default getFloor response is empty object so should not break and not send hard_floor bidderRequest.bids[0].getFloor = () => getFloorResponse; + sinon.spy(bidderRequest.bids[0], 'getFloor'); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // make sure banner bid called with right stuff + expect( + bidderRequest.bids[0].getFloor.calledWith({ + currency: 'USD', + mediaType: 'banner', + size: '*' + }) + ).to.be.true; + let data = parseQuery(request.data); expect(data.rp_hard_floor).to.be.undefined; @@ -1306,6 +1321,7 @@ describe('the rubicon adapter', function () { clonedBid.userId = { tdid: 'abcd-efgh-ijkl-mnop-1234' }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1320,6 +1336,7 @@ describe('the rubicon adapter', function () { lipbid: '0000-1111-2222-3333' } }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1334,6 +1351,7 @@ describe('the rubicon adapter', function () { segments: ['segD', 'segE'] } }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); const unescapedData = unescape(request.data); @@ -1343,18 +1361,53 @@ describe('the rubicon adapter', function () { }); describe('LiveRamp support', function () { - it('should send tpid_liveramp.com when userId defines idl_env', function () { + it('should send x_liverampidl when userId defines idl_env', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { idl_env: '1111-2222-3333-4444' }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); - expect(data['tpid_liveramp.com']).to.equal('1111-2222-3333-4444'); + expect(data['x_liverampidl']).to.equal('1111-2222-3333-4444'); }); }); - }) + + describe('SharedID support', function () { + it('should send sharedid when userId defines sharedId', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + sharedid: { + id: '1111', + third: '2222' + } + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_sharedid.org']).to.equal('1111^3^2222'); + }); + }); + + describe('Config user.id support', function () { + it('should send ppuid when config defines user.id', function () { + config.setConfig({ user: { id: '123' } }); + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + sharedid: { + id: '1111', + third: '2222' + } + }; + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['ppuid']).to.equal('123'); + }); + }); + }); describe('Prebid AdSlot', function () { beforeEach(function () { @@ -1540,19 +1593,44 @@ describe('the rubicon adapter', function () { expect(imp.ext.rubicon.video.skipafter).to.equal(15); expect(imp.ext.prebid.auctiontimestamp).to.equal(1472239426000); expect(post.user.ext.consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + // EIDs should exist + expect(post.user.ext).to.have.property('eids').that.is.an('array'); + // LiveIntent should exist expect(post.user.ext.eids[0].source).to.equal('liveintent.com'); expect(post.user.ext.eids[0].uids[0].id).to.equal('0000-1111-2222-3333'); - expect(post.user.ext.tpid).that.is.an('object'); + expect(post.user.ext.eids[0].uids[0].atype).to.equal(1); + expect(post.user.ext.eids[0]).to.have.property('ext').that.is.an('object'); + expect(post.user.ext.eids[0].ext).to.have.property('segments').that.is.an('array'); + expect(post.user.ext.eids[0].ext.segments[0]).to.equal('segA'); + expect(post.user.ext.eids[0].ext.segments[1]).to.equal('segB'); + // Non-EID properties set using liveintent EID values + expect(post.user.ext).to.have.property('tpid').that.is.an('object'); expect(post.user.ext.tpid.source).to.equal('liveintent.com'); expect(post.user.ext.tpid.uid).to.equal('0000-1111-2222-3333'); + expect(post).to.have.property('rp').that.is.an('object'); + expect(post.rp).to.have.property('target').that.is.an('object'); + expect(post.rp.target).to.have.property('LIseg').that.is.an('array'); + expect(post.rp.target.LIseg[0]).to.equal('segA'); + expect(post.rp.target.LIseg[1]).to.equal('segB'); // LiveRamp should exist expect(post.user.ext.eids[1].source).to.equal('liveramp.com'); expect(post.user.ext.eids[1].uids[0].id).to.equal('1111-2222-3333-4444'); - expect(post.rp).that.is.an('object'); - expect(post.rp.target).that.is.an('object'); - expect(post.rp.target.LIseg).that.is.an('array'); - expect(post.rp.target.LIseg[0]).to.equal('segA'); - expect(post.rp.target.LIseg[1]).to.equal('segB'); + expect(post.user.ext.eids[1].uids[0].atype).to.equal(1); + + // SharedId should exist + expect(post.user.ext.eids[2].source).to.equal('sharedid.org'); + expect(post.user.ext.eids[2].uids[0].id).to.equal('1111'); + expect(post.user.ext.eids[2].uids[0].atype).to.equal(1); + expect(post.user.ext.eids[2].uids[0].ext.third).to.equal('2222'); + // UnifiedId should exist + expect(post.user.ext.eids[3].source).to.equal('adserver.org'); + expect(post.user.ext.eids[3].uids[0].atype).to.equal(1); + expect(post.user.ext.eids[3].uids[0].id).to.equal('3000'); + // PubCommonId should exist + expect(post.user.ext.eids[4].source).to.equal('pubcid.org'); + expect(post.user.ext.eids[4].uids[0].atype).to.equal(1); + expect(post.user.ext.eids[4].uids[0].id).to.equal('4000'); + expect(post.regs.ext.gdpr).to.equal(1); expect(post.regs.ext.us_privacy).to.equal('1NYN'); expect(post).to.have.property('ext').that.is.an('object'); @@ -1568,12 +1646,23 @@ describe('the rubicon adapter', function () { createVideoBidderRequest(); // default getFloor response is empty object so should not break and not send hard_floor bidderRequest.bids[0].getFloor = () => getFloorResponse; + sinon.spy(bidderRequest.bids[0], 'getFloor'); + sandbox.stub(Date, 'now').callsFake(() => bidderRequest.auctionStart + 100 ); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + // make sure banner bid called with right stuff + expect( + bidderRequest.bids[0].getFloor.calledWith({ + currency: 'USD', + mediaType: 'video', + size: [640, 480] + }) + ).to.be.true; + // not an object should work and not send expect(request.data.imp[0].bidfloor).to.be.undefined; @@ -1597,6 +1686,30 @@ describe('the rubicon adapter', function () { [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(request.data.imp[0].bidfloor).to.equal(1.23); }); + + it('should continue with auction and log error if getFloor throws one', function () { + createVideoBidderRequest(); + // default getFloor response is empty object so should not break and not send hard_floor + bidderRequest.bids[0].getFloor = () => { + throw new Error('An exception!'); + }; + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); + + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // log error called + expect(logErrorSpy.calledOnce).to.equal(true); + + // should have an imp + expect(request.data.imp).to.exist.and.to.be.a('array'); + expect(request.data.imp).to.have.lengthOf(1); + + // should be NO bidFloor + expect(request.data.imp[0].bidfloor).to.be.undefined; + }); + it('should add alias name to PBS Request', function() { createVideoBidderRequest(); @@ -1949,6 +2062,56 @@ describe('the rubicon adapter', function () { const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(request.data.ext.prebid.bidders.rubicon.integration).to.equal('testType'); }); + + it('should pass the user.id provided in the config', function () { + config.setConfig({ user: { id: '123' } }); + createVideoBidderRequest(); + + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); + + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; + + expect(post).to.have.property('imp') + // .with.length.of(1); + let imp = post.imp[0]; + expect(imp.id).to.equal(bidderRequest.bids[0].adUnitCode); + expect(imp.exp).to.equal(300); + expect(imp.video.w).to.equal(640); + expect(imp.video.h).to.equal(480); + expect(imp.video.pos).to.equal(1); + expect(imp.video.context).to.equal('instream'); + expect(imp.video.minduration).to.equal(15); + expect(imp.video.maxduration).to.equal(30); + expect(imp.video.startdelay).to.equal(0); + expect(imp.video.skip).to.equal(1); + expect(imp.video.skipafter).to.equal(15); + expect(imp.ext.rubicon.video.playerWidth).to.equal(640); + expect(imp.ext.rubicon.video.playerHeight).to.equal(480); + expect(imp.ext.rubicon.video.size_id).to.equal(201); + expect(imp.ext.rubicon.video.language).to.equal('en'); + // Also want it to be in post.site.content.language + expect(post.site.content.language).to.equal('en'); + expect(imp.ext.rubicon.video.skip).to.equal(1); + expect(imp.ext.rubicon.video.skipafter).to.equal(15); + expect(imp.ext.prebid.auctiontimestamp).to.equal(1472239426000); + expect(post.user.ext.consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + + // Config user.id + expect(post.user.id).to.equal('123'); + + expect(post.regs.ext.gdpr).to.equal(1); + expect(post.regs.ext.us_privacy).to.equal('1NYN'); + expect(post).to.have.property('ext').that.is.an('object'); + expect(post.ext.prebid.targeting.includewinners).to.equal(true); + expect(post.ext.prebid).to.have.property('cache').that.is.an('object'); + expect(post.ext.prebid.cache).to.have.property('vastxml').that.is.an('object'); + expect(post.ext.prebid.cache.vastxml).to.have.property('returnCreative').that.is.an('boolean'); + expect(post.ext.prebid.cache.vastxml.returnCreative).to.equal(false); + expect(post.ext.prebid.bidders.rubicon.integration).to.equal(PBS_INTEGRATION); + }) }); describe('combineSlotUrlParams', function () { diff --git a/test/spec/modules/smartxBidAdapter_spec.js b/test/spec/modules/smartxBidAdapter_spec.js new file mode 100644 index 00000000000..efc6abcc5fa --- /dev/null +++ b/test/spec/modules/smartxBidAdapter_spec.js @@ -0,0 +1,513 @@ +import { + expect +} from 'chai'; +import { + spec +} from 'modules/smartxBidAdapter.js'; + +describe('The smartx adapter', function () { + function getValidBidObject() { + return { + bidId: 123, + mediaTypes: { + video: { + // context: 'outstream', + playerSize: [ + ['640', '360'] + ] + } + }, + params: { + tagId: 'Nu68JuOWAvrbzoyrOR9a7A', + publisherId: '__name__', + siteId: '__name__', + bidfloor: 0.3, + bidfloorcur: 'EUR', + // user: { + // data: '' + // } + // outstream_options: { + // slot: 'yourelementid' + // } + } + + }; + }; + + describe('isBidRequestValid', function () { + var bid; + + beforeEach(function () { + bid = getValidBidObject(); + }); + + it('should fail validation if the bid isn\'t defined or not an object', function () { + var result = spec.isBidRequestValid(); + + expect(result).to.equal(false); + + result = spec.isBidRequestValid('not an object'); + + expect(result).to.equal(false); + }); + + it('should succeed validation with all the right parameters', function () { + expect(spec.isBidRequestValid(getValidBidObject())).to.equal(true); + }); + + it('should succeed validation with mediaType and outstream_function or outstream_options', function () { + bid.mediaType = 'video'; + bid.params.outstream_function = 'outstream_func'; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + + delete bid.params.outstream_function; + bid.params.outstream_options = { + slot: 'yourelementid' + }; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should succeed with ad_unit outstream and outstream function set', function () { + bid.params.ad_unit = 'outstream'; + bid.params.outstream_function = function () {}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should succeed with mediaTypes_video_context outstream, options set for outstream and slot provided', function () { + bid.mediaTypes.video.context = 'outstream'; + bid.params.outstream_options = { + slot: 'yourelementid' + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should fail without video', function () { + delete bid.mediaTypes.video; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without playerSize', function () { + delete bid.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without tagId', function () { + delete bid.params.tagId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without publisherId', function () { + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without siteId', function () { + delete bid.params.siteId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without bidfloor', function () { + delete bid.params.bidfloor; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without bidfloorcur', function () { + delete bid.params.bidfloorcur; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail with context outstream but no options set for outstream', function () { + bid.mediaTypes.video.context = 'outstream'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail with context outstream, options set for outstream but no slot provided', function () { + bid.mediaTypes.video.context = 'outstream'; + bid.params.outstream_options = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should succeed with context outstream, options set for outstream but no outstream_function is set', function () { + bid.mediaTypes.video.context = 'outstream'; + bid.params.outstream_options = { + slot: 'yourelementid' + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + var bid, + bidRequestObj; + + beforeEach(function () { + bid = getValidBidObject(); + bidRequestObj = { + refererInfo: { + referer: 'prebid.js' + } + }; + }); + + it('should build a very basic request', function () { + var request = spec.buildRequests([bid], bidRequestObj)[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://bid.sxp.smartclip.net/bid/1000'); + expect(request.bidRequest).to.equal(bidRequestObj); + expect(request.data.imp.id).to.match(/\d+/); + expect(request.data.imp.secure).to.equal(0); + + expect(request.data.imp.video).to.deep.equal({ + ext: { + sdk_name: 'Prebid 1+' + }, + h: '360', + w: '640', + mimes: [ + 'application/javascript', 'video/mp4', 'video/webm' + ], + api: [2], + delivery: [2], + linearity: 1, + maxbitrate: 3500, + maxduration: 500, + minbitrate: 0, + minduration: 0, + protocols: [ + 2, 3, 5, 6 + ], + startdelay: 0, + placement: 1, + pos: 1 + }); + + expect(request.data.site).to.deep.equal({ + content: 'content', + id: '__name__', + page: 'prebid.js', + cat: '', + domain: '', + publisher: { + id: '__name__' + } + }); + }); + + it('should change request parameters based on options sent', function () { + var request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.imp.video.ext).to.deep.equal({ + sdk_name: 'Prebid 1+' + }); + + expect(request.data.imp.video).to.contain({ + placement: 1 + }); + + bid.mediaTypes.video.context = 'outstream'; + + bid.params = { + outstream_options: { + foo: 'bar' + }, + outstream_function: '987', + mimes: 'foo', + linearity: 2, + minduration: 5, + maxduration: 10, + startdelay: 1, + minbitrate: 50, + maxbitrate: 500, + delivery: [1], + pos: 2, + api: [1], + protocols: [ + 2, 3, 5 + ], + bidfloor: 55, + bidfloorcur: 'foo', + at: 1, + cur: ['FOO'] + }; + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.imp.video.ext).to.deep.equal({ + sdk_name: 'Prebid 1+' + }); + + expect(request.data.imp.video).to.contain({ + minduration: 5, + maxduration: 10 + }); + + expect(request.data.imp.video.startdelay).to.equal(1); + + expect(request.data.imp.video).to.contain({ + placement: 3 + }); + + expect(request.data.imp.bidfloor).to.equal(55); + + expect(request.data.imp.bidfloorcur).to.equal('foo'); + + expect(request.data.imp.video.linearity).to.equal(2); + + expect(request.data.imp.video.minbitrate).to.equal(50); + + expect(request.data.imp.video.maxbitrate).to.equal(500); + }); + + it('should pass GDPR params', function () { + var request; + + bidRequestObj.gdprConsent = { + gdprApplies: true, + consentString: 'foo' + } + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.regs.ext.gdpr).to.equal(1); + expect(request.data.user.ext.consent).to.be.an('string'); + expect(request.data.user.ext.consent).to.equal('foo'); + }); + + it('should pass emq params', function () { + var request; + + bid.params.user = { + data: [{ + id: 'emq', + name: 'emq', + segment: [{ + name: 'emq', + value: 'foo' + }] + }] + } + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.user.data).to.deep.equal([{ + id: 'emq', + name: 'emq', + segment: { + name: 'emq', + value: 'foo' + } + }]); + }); + + it('should pass crumbs params', function () { + var request; + + bid.crumbs = { + pubcid: 'pubcid_1' + }; + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.user.ext).to.contain({ + fpc: 'pubcid_1' + }); + }); + + it('should pass linearity params', function () { + var request; + + bid.params.linearity = 3 + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.imp.video.linearity).to.equal(3); + }); + + it('should pass min and max duration params', function () { + var request; + + bid.params.minduration = 3 + bid.params.maxduration = 15 + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.imp.video.minduration).to.equal(3); + expect(request.data.imp.video.maxduration).to.equal(15); + }); + }); + + describe('interpretResponse', function () { + var serverResponse, bidderRequestObj; + + beforeEach(function () { + bidderRequestObj = { + bidRequest: { + bids: [{ + mediaTypes: { + video: { + playerSize: [ + ['400', '300'] + ] + } + }, + bidId: 123, + params: { + player_width: 400, + player_height: 300, + content_page_url: 'prebid.js', + ad_mute: 1, + outstream_options: { + foo: 'bar' + }, + outstream_function: 'function', + } + }, { + mediaTypes: { + video: { + playerSize: [ + ['200', '100'] + ] + } + }, + bidId: 124, + params: { + player_width: 200, + player_height: 100, + content_page_url: 'prebid.js', + ad_mute: 1, + outstream_options: { + foo: 'bar' + }, + outstream_function: 'function' + } + }] + } + }; + + serverResponse = { + body: { + id: 12345, + seatbid: [{ + bid: [{ + impid: 123, + cur: 'USD', + price: 12, + adomain: ['abc.com'], + crid: 321, + w: 400, + h: 300, + ext: { + slot: 'slot123' + } + }, { + impid: 124, + cur: 'USD', + price: 13, + adomain: ['def.com'], + crid: 654, + w: 200, + h: 100, + ext: { + slot: 'slot124' + } + }] + }] + } + }; + }); + + it('should return an array of bid responses', function () { + var responses = spec.interpretResponse(serverResponse, bidderRequestObj); + expect(responses).to.be.an('array').with.length(2); + expect(responses[0].requestId).to.equal(123); + expect(responses[0].currency).to.equal('USD'); + expect(responses[0].cpm).to.equal(12); + expect(responses[0].creativeId).to.equal(321); + expect(responses[0].ttl).to.equal(360); + expect(responses[0].netRevenue).to.equal(true); + expect(responses[0].mediaType).to.equal('video'); + expect(responses[0].width).to.equal(400); + expect(responses[0].height).to.equal(300); + expect(responses[1].requestId).to.equal(124); + expect(responses[1].currency).to.equal('USD'); + expect(responses[1].cpm).to.equal(13); + expect(responses[1].creativeId).to.equal(654); + expect(responses[1].ttl).to.equal(360); + expect(responses[1].netRevenue).to.equal(true); + expect(responses[1].mediaType).to.equal('video'); + expect(responses[1].width).to.equal(200); + expect(responses[1].height).to.equal(100); + }); + }); + + describe('oustreamRender', function () { + var serverResponse, bidderRequestObj; + + beforeEach(function () { + bidderRequestObj = { + bidRequest: { + bids: [{ + mediaTypes: { + video: { + context: 'outstream', + playerSize: [ + ['400', '300'] + ] + } + }, + bidId: 123, + params: { + player_width: 400, + player_height: 300, + content_page_url: 'prebid.js', + ad_mute: 1, + outstream_options: { + slot: 'slot123' + }, + outstream_function: 'function', + } + }] + } + }; + + serverResponse = { + body: { + id: 12345, + seatbid: [{ + bid: [{ + impid: 123, + cur: 'USD', + price: 12, + adomain: ['abc.com'], + crid: 321, + w: 400, + h: 300, + ext: { + slot: 'slot123' + } + }] + }] + } + }; + }); + + it('should attempt to insert the EASI script', function () { + var scriptTag; + sinon.stub(window.document, 'getElementById').returns({ + appendChild: sinon.stub().callsFake(function (script) { + scriptTag = script + }) + }); + var responses = spec.interpretResponse(serverResponse, bidderRequestObj); + + responses[0].renderer.render(responses[0]); + + expect(scriptTag.getAttribute('type')).to.equal('text/javascript'); + expect(scriptTag.getAttribute('src')).to.equal('https://dco.smartclip.net/?plc=7777777'); + + window.document.getElementById.restore(); + }); + }); +}) diff --git a/test/spec/modules/sspBCAdapter_spec.js b/test/spec/modules/sspBCAdapter_spec.js new file mode 100644 index 00000000000..2cb0e8defa4 --- /dev/null +++ b/test/spec/modules/sspBCAdapter_spec.js @@ -0,0 +1,334 @@ +import { assert, expect } from 'chai'; +import { spec } from 'modules/sspBCAdapter.js'; +import * as utils from 'src/utils.js'; + +const BIDDER_CODE = 'sspBC'; +const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; +const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; + +describe('SSPBC adapter', function () { + function prepareTestData() { + const bidderRequestId = '1041bb47b0fafa'; + const auctionId = '8eda6d06-3d7c-4a94-9b35-74e42fbb3089'; + const transactionId = '50259989-b5c0-4edf-8f47-b1ef5fbedf39'; + const gdprConsent = { + consentString: 'BOtq-3dOtq-30BIABCPLC4-AAAAthr_7__7-_9_-_f__9uj3Or_v_f__30ccL59v_h_7v-_7fi_20nV4u_1vft9yfk1-5ctDztp505iakivHmqNeb9v_mz1_5pRP78k89r7337Ew_v8_v-b7JCON_Ig', + gdprApplies: true, + } + const bids = [{ + adUnitCode: 'test_wideboard', + bidder: BIDDER_CODE, + mediaTypes: { + banner: { + sizes: [ + [728, 90], + [750, 100], + [750, 200] + ] + } + }, + sizes: [ + [728, 90], + [750, 100], + [750, 200] + ], + params: { + id: '003', + siteId: '8816', + }, + auctionId, + bidderRequestId, + bidId: auctionId + '1', + transactionId, + }, + { + adUnitCode: 'test_rectangle', + bidder: BIDDER_CODE, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + sizes: [ + [300, 250] + ], + params: { + id: '005', + siteId: '8816', + }, + auctionId, + bidderRequestId, + bidId: auctionId + '1', + transactionId, + } + ]; + const bids_test = [{ + adUnitCode: 'test_wideboard', + bidder: BIDDER_CODE, + mediaTypes: { + banner: { + sizes: [ + [970, 300], + [750, 300], + [750, 200], + [750, 100], + [300, 250] + ] + } + }, + sizes: [ + [970, 300], + [750, 300], + [750, 200], + [750, 100], + [300, 250] + ], + params: { + id: '005', + siteId: '235911', + test: 1 + }, + auctionId, + bidderRequestId, + bidId: auctionId + '1', + transactionId, + }]; + const bidRequest = { + auctionId, + bidderCode: BIDDER_CODE, + bidderRequestId, + bids, + gdprConsent, + refererInfo: { + reachedTop: true, + referer: 'https://test.site.pl/', + stack: ['https://test.site.pl/'], + } + }; + const bidRequestSingle = { + auctionId, + bidderCode: BIDDER_CODE, + bidderRequestId, + bids: [bids[0]], + gdprConsent, + refererInfo: { + reachedTop: true, + referer: 'https://test.site.pl/', + stack: ['https://test.site.pl/'], + } + }; + const bidRequestTest = { + auctionId, + bidderCode: BIDDER_CODE, + bidderRequestId, + bids: bids_test, + gdprConsent, + refererInfo: { + reachedTop: true, + referer: 'https://test.site.pl/', + stack: ['https://test.site.pl/'], + } + }; + const bidRequestTestNoGDPR = { + auctionId, + bidderCode: BIDDER_CODE, + bidderRequestId, + bids: bids_test, + refererInfo: { + reachedTop: true, + referer: 'https://test.site.pl/', + stack: ['https://test.site.pl/'], + } + }; + const serverResponse = { + 'body': { + 'id': auctionId, + 'seatbid': [{ + 'bid': [{ + 'id': '3347324c-6889-46d2-a800-ae78a5214c06', + 'impid': '003', + 'price': 1, + 'adid': 'lxHWkB7OnZeso3QiN1N4', + 'nurl': '', + 'adm': 'AD CODE 1', + 'adomain': ['adomain.pl'], + 'cid': 'BZ4gAg21T5nNtxlUCDSW', + 'crid': 'lxHWkB7OnZeso3QiN1N4', + 'w': 728, + 'h': 90 + }], + 'seat': 'dsp1', + 'group': 0 + }, { + 'bid': [{ + 'id': '2d766853-ea07-4529-8299-5f0ebadc546a', + 'impid': '005', + 'price': 2, + 'adm': 'AD CODE 2', + 'cid': '57744', + 'crid': '858252', + 'w': 300, + 'h': 250 + }], + 'seat': 'dsp2', + 'group': 0 + }], + 'cur': 'PLN' + } + }; + const serverResponseSingle = { + 'body': { + 'id': auctionId, + 'seatbid': [{ + 'bid': [{ + 'id': '3347324c-6889-46d2-a800-ae78a5214c06', + 'impid': '003', + 'price': 1, + 'adid': 'lxHWkB7OnZeso3QiN1N4', + 'nurl': '', + 'adm': 'AD CODE 1', + 'adomain': ['adomain.pl'], + 'cid': 'BZ4gAg21T5nNtxlUCDSW', + 'crid': 'lxHWkB7OnZeso3QiN1N4', + 'w': 728, + 'h': 90 + }], + 'seat': 'dsp1', + 'group': 0 + }], + 'cur': 'PLN' + } + }; + const emptyResponse = { + 'body': { + 'id': auctionId, + } + } + return { + bids, + bids_test, + bidRequest, + bidRequestSingle, + bidRequestTest, + bidRequestTestNoGDPR, + serverResponse, + serverResponseSingle, + emptyResponse + }; + }; + + describe('dependencies', function () { + it('utils should contain required functions', function () { + expect(utils.parseUrl).to.be.a('function'); + expect(utils.deepAccess).to.be.a('function'); + expect(utils.logWarn).to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const { bids } = prepareTestData(); + let bid = bids[0]; + + it('should return true when required params found', function () { + assert(spec.isBidRequestValid(bid)); + }); + + it('should return false when required params are missing', function () { + bid.params.id = undefined; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + }); + + describe('buildRequests', function () { + const { bids, bidRequest, bidRequestSingle } = prepareTestData(); + const request = spec.buildRequests(bids, bidRequest); + const requestSingle = spec.buildRequests([bids[0]], bidRequestSingle); + const payload = request ? JSON.parse(request.data) : { site: false, imp: false }; + const payloadSingle = request ? JSON.parse(requestSingle.data) : { site: false, imp: false }; + + it('should send bid request to endpoint via POST', function () { + expect(request.url).to.contain(BIDDER_URL); + expect(request.method).to.equal('POST'); + }); + + it('should contain prebid and bidder versions', function () { + expect(request.url).to.contain('bdver'); + expect(request.url).to.contain('pbver=$prebid.version$'); + }); + + it('should create one imp object per bid', function () { + expect(payload.imp.length).to.equal(bids.length); + expect(payloadSingle.imp.length).to.equal(1); + }); + + it('should save bidder request data', function () { + expect(request.bidderRequest).to.deep.equal(bidRequest); + }); + + it('should send site Id from bidder params', function () { + expect(payload.site.id).to.equal(bids[0].params.siteId); + }); + + it('should send page url from refererInfo', function () { + expect(payload.site.page).to.equal(bidRequest.refererInfo.referer); + }); + + it('should send gdpr data', function () { + expect(payload.regs).to.be.an('object').and.to.have.property('[ortb_extensions.gdpr]', 1); + expect(payload.user).to.be.an('object').and.to.have.property('[ortb_extensions.consent]', bidRequest.gdprConsent.consentString); + }); + }); + + describe('interpretResponse', function () { + const { bids, emptyResponse, serverResponse, serverResponseSingle, bidRequest, bidRequestSingle } = prepareTestData(); + const request = spec.buildRequests(bids, bidRequest); + const requestSingle = spec.buildRequests([bids[0]], bidRequestSingle); + + it('should handle nobid responses', function () { + let result = spec.interpretResponse(emptyResponse, request); + expect(result.length).to.equal(0); + }); + + it('should create bids from non-empty responses', function () { + let result = spec.interpretResponse(serverResponse, request); + let resultSingle = spec.interpretResponse(serverResponseSingle, requestSingle); + + expect(result.length).to.equal(bids.length); + expect(resultSingle.length).to.equal(1); + expect(resultSingle[0]).to.have.keys('ad', 'cpm', 'width', 'height', 'bidderCode', 'mediaType', 'meta', 'requestId', 'creativeId', 'currency', 'netRevenue', 'ttl'); + }); + + it('should handle a partial response', function () { + let resultPartial = spec.interpretResponse(serverResponseSingle, request); + expect(resultPartial.length).to.equal(1); + }); + + it('banner ad code should contain required variables', function () { + let resultSingle = spec.interpretResponse(serverResponseSingle, requestSingle); + let adcode = resultSingle[0].ad; + expect(adcode).to.be.a('string'); + expect(adcode).to.contain('window.rekid'); + expect(adcode).to.contain('window.mcad'); + expect(adcode).to.contain('window.gdpr'); + expect(adcode).to.contain('window.page'); + }) + }); + + describe('getUserSyncs', function () { + let syncResultAll = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }); + let syncResultImage = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: true }); + let syncResultNone = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: false }); + + it('should provide correct url, if frame sync is allowed', function () { + expect(syncResultAll).to.have.length(1); + expect(syncResultAll[0].url).to.be.equal(SYNC_URL); + }); + + it('should send no syncs, if frame sync is not allowed', function () { + expect(syncResultImage).to.be.undefined; + expect(syncResultNone).to.be.undefined; + }); + }); +}); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index d9671aabc84..5ac68de345d 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -23,6 +23,7 @@ import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; import {id5IdSubmodule} from 'modules/id5IdSystem.js'; import {identityLinkSubmodule} from 'modules/identityLinkIdSystem.js'; import {liveIntentIdSubmodule} from 'modules/liveIntentIdSystem.js'; +import {merkleIdSubmodule} from 'modules/merkleIdSystem.js'; import {netIdSubmodule} from 'modules/netIdSystem.js'; import {intentIqIdSubmodule} from 'modules/intentIqIdSystem.js'; import {sharedIdSubmodule} from 'modules/sharedIdSystem.js'; @@ -34,7 +35,7 @@ const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; const CONSENT_LOCAL_STORAGE_NAME = '_pbjs_userid_consent_data'; describe('User ID', function() { - function getConfigMock(configArr1, configArr2, configArr3, configArr4, configArr5, configArr6, configArr7, configArr8) { + function getConfigMock(configArr1, configArr2, configArr3, configArr4, configArr5, configArr6, configArr7, configArr8, configArr9) { return { userSync: { syncDelay: 0, @@ -46,7 +47,8 @@ describe('User ID', function() { (configArr5 && configArr5.length >= 3) ? getStorageMock.apply(null, configArr5) : null, (configArr6 && configArr6.length >= 3) ? getStorageMock.apply(null, configArr6) : null, (configArr7 && configArr7.length >= 3) ? getStorageMock.apply(null, configArr7) : null, - (configArr8 && configArr8.length >= 3) ? getStorageMock.apply(null, configArr8) : null + (configArr8 && configArr8.length >= 3) ? getStorageMock.apply(null, configArr8) : null, + (configArr9 && configArr9.length >= 3) ? getStorageMock.apply(null, configArr9) : null ].filter(i => i) } } @@ -352,7 +354,7 @@ describe('User ID', function() { }); it('handles config with no usersync object', function() { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule]); init(config); config.setConfig({}); // usersync is undefined, and no logInfo message for 'User ID - usersync config updated' @@ -360,14 +362,14 @@ describe('User ID', function() { }); it('handles config with empty usersync object', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule]); init(config); config.setConfig({userSync: {}}); expect(typeof utils.logInfo.args[0]).to.equal('undefined'); }); it('handles config with usersync and userIds that are empty objs', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -378,7 +380,7 @@ describe('User ID', function() { }); it('handles config with usersync and userIds with empty names or that dont match a submodule.name', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -1116,6 +1118,30 @@ describe('User ID', function() { }, {adUnits}); }); + it('test hook from merkleId cookies', function(done) { + // simulate existing browser local storage values + coreStorage.setCookie('merkleId', JSON.stringify({'ppid': {'id': 'testmerkleId'}}), (new Date(Date.now() + 5000).toUTCString())); + + setSubmoduleRegistry([merkleIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['merkleId', 'merkleId', 'cookie'])); + + requestBidsHook(function() { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.merkleId'); + expect(bid.userId.merkleId).to.equal('testmerkleId'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'merkleinc.com', + uids: [{id: 'testmerkleId', atype: 1}] + }); + }); + }); + coreStorage.setCookie('merkleId', '', EXPIRED_COOKIE_DATE); + done(); + }, {adUnits}); + }); + it('test hook when pubCommonId, unifiedId, id5Id, identityLink, britepoolId, intentIqId, sharedId and netId have data to pass', function(done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'testunifiedid'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1591,13 +1617,6 @@ describe('User ID', function() { sinon.assert.notCalled(mockGetId); sinon.assert.calledOnce(mockDecode); sinon.assert.calledOnce(mockExtendId); - - let consent = gdprDataHandler.getConsentData(); - let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); - expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); - expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); - expect(userIdStoredConsent.consentString).to.equal(consent.consentString); - expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); }); it('calls getId if no stored consent data but refresh is needed', function () { @@ -1611,13 +1630,6 @@ describe('User ID', function() { sinon.assert.calledOnce(mockGetId); sinon.assert.calledOnce(mockDecode); sinon.assert.notCalled(mockExtendId); - - let consent = gdprDataHandler.getConsentData(); - let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); - expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); - expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); - expect(userIdStoredConsent.consentString).to.equal(consent.consentString); - expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); }); it('calls getId if empty stored consent and refresh not needed', function () { @@ -1633,13 +1645,6 @@ describe('User ID', function() { sinon.assert.calledOnce(mockGetId); sinon.assert.calledOnce(mockDecode); sinon.assert.notCalled(mockExtendId); - - let consent = gdprDataHandler.getConsentData(); - let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); - expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); - expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); - expect(userIdStoredConsent.consentString).to.equal(consent.consentString); - expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); }); it('calls getId if stored consent does not match current consent and refresh not needed', function () { @@ -1659,13 +1664,6 @@ describe('User ID', function() { sinon.assert.calledOnce(mockGetId); sinon.assert.calledOnce(mockDecode); sinon.assert.notCalled(mockExtendId); - - let consent = gdprDataHandler.getConsentData(); - let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); - expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); - expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); - expect(userIdStoredConsent.consentString).to.equal(consent.consentString); - expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); }); it('does not call getId if stored consent matches current consent and refresh not needed', function () { @@ -1685,13 +1683,6 @@ describe('User ID', function() { sinon.assert.notCalled(mockGetId); sinon.assert.calledOnce(mockDecode); sinon.assert.calledOnce(mockExtendId); - - let consent = gdprDataHandler.getConsentData(); - let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); - expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); - expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); - expect(userIdStoredConsent.consentString).to.equal(consent.consentString); - expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); }); }); }); diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 57e7f4e8ae2..8b3a492d2e5 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -315,12 +315,7 @@ describe('VidazooBidAdapter', function () { describe('unique deal id', function () { const key = 'myKey'; let uniqueDealId; - - it('should get fresh unique deal id', function () { - const now = Date.now(); - uniqueDealId = getUniqueDealId(key); - expect(uniqueDealId).to.be.equal(`${key}_${now.toString()}`); - }); + uniqueDealId = getUniqueDealId(key); it('should get current unique deal id', function (done) { // waiting some time so `now` will become past diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index 1e20343c1cd..e7a9285cb48 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -47,6 +47,16 @@ const VIDEO_RESPONSE = Object.assign({}, RESPONSE, { 'adtype': 'VIDEO' }) +const REQPARAMS = { + json: true, + ts: 1234567890 +} + +const REQPARAMS_GDPR = Object.assign({}, REQPARAMS, { + gdpr: true, + consent: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA' +}) + describe('yieldlabBidAdapter', function () { const adapter = newBidder(spec) @@ -131,7 +141,7 @@ describe('yieldlabBidAdapter', function () { }) it('should get correct bid response', function () { - const result = spec.interpretResponse({body: [RESPONSE]}, {validBidRequests: [REQUEST]}) + const result = spec.interpretResponse({body: [RESPONSE]}, {validBidRequests: [REQUEST], queryParams: REQPARAMS}) expect(result[0].requestId).to.equal('2d925f27f5079f') expect(result[0].cpm).to.equal(0.01) @@ -147,6 +157,13 @@ describe('yieldlabBidAdapter', function () { expect(result[0].ad).to.include('&id=abc') }) + it('should append gdpr parameters to adtag', function () { + const result = spec.interpretResponse({body: [RESPONSE]}, {validBidRequests: [REQUEST], queryParams: REQPARAMS_GDPR}) + + expect(result[0].ad).to.include('&gdpr=true') + expect(result[0].ad).to.include('&consent=BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA') + }) + it('should get correct bid response when passing more than one size', function () { const REQUEST2 = Object.assign({}, REQUEST, { 'sizes': [ @@ -155,7 +172,7 @@ describe('yieldlabBidAdapter', function () { [970, 90], ] }) - const result = spec.interpretResponse({body: [RESPONSE]}, {validBidRequests: [REQUEST2]}) + const result = spec.interpretResponse({body: [RESPONSE]}, {validBidRequests: [REQUEST2], queryParams: REQPARAMS}) expect(result[0].requestId).to.equal('2d925f27f5079f') expect(result[0].cpm).to.equal(0.01) @@ -179,7 +196,7 @@ describe('yieldlabBidAdapter', function () { } } }) - const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [VIDEO_REQUEST]}) + const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [VIDEO_REQUEST], queryParams: REQPARAMS}) expect(result[0].requestId).to.equal('2d925f27f5079f') expect(result[0].cpm).to.equal(0.01) @@ -188,6 +205,20 @@ describe('yieldlabBidAdapter', function () { expect(result[0].vastUrl).to.include('&id=abc') }) + it('should append gdpr parameters to vastUrl', function () { + const VIDEO_REQUEST = Object.assign({}, REQUEST, { + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + }) + const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [VIDEO_REQUEST], queryParams: REQPARAMS_GDPR}) + + expect(result[0].vastUrl).to.include('&gdpr=true') + expect(result[0].vastUrl).to.include('&consent=BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA') + }) + it('should add renderer if outstream context', function () { const OUTSTREAM_REQUEST = Object.assign({}, REQUEST, { 'mediaTypes': { @@ -197,7 +228,7 @@ describe('yieldlabBidAdapter', function () { } } }) - const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [OUTSTREAM_REQUEST]}) + const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [OUTSTREAM_REQUEST], queryParams: REQPARAMS}) expect(result[0].renderer.id).to.equal('2d925f27f5079f') expect(result[0].renderer.url).to.equal('https://ad2.movad.net/dynamic.ad?a=o193092&ma_loadEvent=ma-start-event') diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index fca59633ebe..6494ead78e7 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -1177,5 +1177,25 @@ describe('Utils', function () { } expect(utils.deepEqual(obj1, obj2)).to.equal(false); }); + + describe('cyrb53Hash', function() { + it('should return the same hash for the same string', function() { + const stringOne = 'string1'; + expect(utils.cyrb53Hash(stringOne)).to.equal(utils.cyrb53Hash(stringOne)); + }); + it('should return a different hash for the same string with different seeds', function() { + const stringOne = 'string1'; + expect(utils.cyrb53Hash(stringOne, 1)).to.not.equal(utils.cyrb53Hash(stringOne, 2)); + }); + it('should return a different hash for different strings with the same seed', function() { + const stringOne = 'string1'; + const stringTwo = 'string2'; + expect(utils.cyrb53Hash(stringOne)).to.not.equal(utils.cyrb53Hash(stringTwo)); + }); + it('should return a string value, not a number', function() { + const stringOne = 'string1'; + expect(typeof utils.cyrb53Hash(stringOne)).to.equal('string'); + }); + }); }); });