diff --git a/.babelrc.js b/.babelrc.js index bece57ec4a5..2fa95d0716e 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -20,7 +20,7 @@ module.exports = { "safari >=8", "edge >= 14", "ff >= 57", - "ie >= 10", + "ie >= 11", "ios >= 8" ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 962e057fbc5..606d26cd25a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ Contributions are always welcome. To contribute, [fork](https://help.github.com/ commit your changes, and [open a pull request](https://help.github.com/articles/using-pull-requests/) against the master branch. -Pull requests must have 80% code coverage before beign considered for merge. +Pull requests must have 80% code coverage before being 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). @@ -59,7 +59,7 @@ When you are adding code to Prebid.js, or modifying code that isn't covered by a Prebid.js already has many tests. Read them to see how Prebid.js is tested, and for inspiration: - Look in `test/spec` and its subdirectories -- Tests for bidder adaptors are located in `test/spec/modules` +- Tests for bidder adapters are located in `test/spec/modules` A test module might have the following general structure: diff --git a/PR_REVIEW.md b/PR_REVIEW.md index a8b68c7ab45..f991a0254f5 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -31,7 +31,7 @@ Follow steps above for general review process. In addition, please verify the fo - 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 that accept a floor parameter must also support the [floors module](https://docs.prebid.org/dev-docs/modules/floors.html) -- look for a call to the `getFloor()` function. - Adapters cannot accept an schain parameter. Rather, they must look for the schain parameter at bidRequest.schain. - The 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'); @@ -89,9 +89,14 @@ Documentation they're supposed to be following is https://docs.prebid.org/dev-do Follow steps above for general review process. In addition: - The RTD Provider must include a `providerRtdProvider.md` file. This file must have example parameters and document a sense of what to expect: what should change in the bidrequest, or what targeting data should be added? - Try running the new sub-module and confirm the provided test parameters. -- Make sure the sub-module is making HTTP requests as early as possible, but not more often than needed. +- Confirm that the module + - is not loading external code. If it is, escalate to the #prebid-js Slack channel. + - is reading `config` from the function signature rather than calling `getConfig`. + - is sending data to the bid request only as either First Party Data or in bidRequest.rtd.RTDPROVIDERCODE. + - is making HTTPS requests as early as possible, but not more often than needed. + - doesn't force bid adapters to load additional code. - Consider whether the kind of data the module is obtaining could have privacy implications. If so, make sure they're utilizing the `consent` data passed to them. -- make sure there's a docs pull request +- Make sure there's a docs pull request ## Ticket Coordinator diff --git a/README.md b/README.md index 44882570d89..40df62ccee4 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ This runs some code quality checks, starts a web server at `http://localhost:999 ### Build Optimization -The standard build output contains all the available modules from within the `modules` folder. +The standard build output contains all the available modules from within the `modules` folder. Note, however that there are bid adapters which support multiple bidders through aliases, so if you don't see a file in modules for a bid adapter, you may need to grep the repository to find the name of the module you need to include. You might want to exclude some/most of them from the final bundle. To make sure the build only includes the modules you want, you can specify the modules to be included with the `--modules` CLI argument. diff --git a/integrationExamples/gpt/audigentSegments_example.html b/integrationExamples/gpt/audigentSegments_example.html deleted file mode 100644 index 7739b558327..00000000000 --- a/integrationExamples/gpt/audigentSegments_example.html +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - -

Audigent Segments Prebid

- -
- -
-TDID: -
-
- -Audigent Segments: -
-
- - diff --git a/integrationExamples/gpt/haloRtdProvider_example.html b/integrationExamples/gpt/haloRtdProvider_example.html new file mode 100644 index 00000000000..7f9a34e55ee --- /dev/null +++ b/integrationExamples/gpt/haloRtdProvider_example.html @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + +

Audigent Segments Prebid

+ +
+ +
+ +Halo Id: +
+
+ +Audigent Segments (Appnexus): +
+
+ + diff --git a/integrationExamples/gpt/jwplayerRtdProvider_example.html b/integrationExamples/gpt/jwplayerRtdProvider_example.html index 3791ab42137..75eb85a2d8c 100644 --- a/integrationExamples/gpt/jwplayerRtdProvider_example.html +++ b/integrationExamples/gpt/jwplayerRtdProvider_example.html @@ -11,10 +11,18 @@ var adUnits = [{ code: 'div-gpt-ad-1460505748561-0', - jwTargeting: { - playerID: '123', - mediaID: 'abc' + fpd: { + context: { + data: { + jwTargeting: { + // Note: the following Ids are placeholders and should be replaced with your Ids. + playerID: '123', + mediaID: 'abc' + } + }, + } }, + mediaTypes: { banner: { sizes: [[300, 250], [300,600]], @@ -32,7 +40,6 @@ var pbjs = pbjs || {}; pbjs.que = pbjs.que || []; - + + + Reconciliation RTD Provider Example + + + + + + + + +
Div-1
+
+ +
+ + diff --git a/modules/.submodules.json b/modules/.submodules.json index 91cda9d95ad..4e02391129a 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -17,7 +17,9 @@ "haloIdSystem", "quantcastIdSystem", "idxIdSystem", - "fabrickIdSystem" + "fabrickIdSystem", + "verizonMediaIdSystem", + "pubProvidedIdSystem" ], "adpod": [ "freeWheelAdserverVideo", @@ -25,7 +27,9 @@ ], "rtdModule": [ "browsiRtdProvider", - "audigentRtdProvider", - "jwplayerRtdProvider" + "haloRtdProvider", + "jwplayerRtdProvider", + "reconciliationRtdProvider", + "geoedgeRtdProvider" ] } diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index 798b6450946..65df8baad2e 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -1,12 +1,37 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import * as utils from '../src/utils.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = '33across'; const END_POINT = 'https://ssc.33across.com/api/v1/hb'; const SYNC_ENDPOINT = 'https://ssc-cms.33across.com/ps/?m=xch&rt=html&ru=deb'; -const MEDIA_TYPE = 'banner'; + const CURRENCY = 'USD'; +const GUID_PATTERN = /^[a-zA-Z0-9_-]{22}$/; + +const PRODUCT = { + SIAB: 'siab', + INVIEW: 'inview', + INSTREAM: 'instream' +}; + +const VIDEO_ORTB_PARAMS = [ + 'mimes', + 'minduration', + 'maxduration', + 'placement', + 'protocols', + 'startdelay', + 'skip', + 'skipafter', + 'minbitrate', + 'maxbitrate', + 'delivery', + 'playbackmethod', + 'api', + 'linearity' +]; const adapterState = { uniqueSiteIds: [] @@ -14,57 +39,122 @@ const adapterState = { const NON_MEASURABLE = 'nm'; -// All this assumes that only one bid is ever returned by ttx -function _createBidResponse(response) { - return { - requestId: response.id, - bidderCode: BIDDER_CODE, - cpm: response.seatbid[0].bid[0].price, - width: response.seatbid[0].bid[0].w, - height: response.seatbid[0].bid[0].h, - ad: response.seatbid[0].bid[0].adm, - ttl: response.seatbid[0].bid[0].ttl || 60, - creativeId: response.seatbid[0].bid[0].crid, - currency: response.cur, - netRevenue: true +// **************************** VALIDATION *************************** // +function isBidRequestValid(bid) { + return ( + _validateBasic(bid) && + _validateBanner(bid) && + _validateVideo(bid) + ); +} + +function _validateBasic(bid) { + if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { + return false; + } + + if (!_validateGUID(bid)) { + return false; } + + return true; } -function _isViewabilityMeasurable(element) { - return !_isIframe() && element !== null; +function _validateGUID(bid) { + const siteID = utils.deepAccess(bid, 'params.siteId', '') || ''; + if (siteID.trim().match(GUID_PATTERN) === null) { + return false; + } + + return true; } -function _getViewability(element, topWin, { w, h } = {}) { - return topWin.document.visibilityState === 'visible' - ? _getPercentInView(element, topWin, { w, h }) - : 0; +function _validateBanner(bid) { + const banner = utils.deepAccess(bid, 'mediaTypes.banner'); + // If there's no banner no need to validate against banner rules + if (banner === undefined) { + return true; + } + + if (!Array.isArray(banner.sizes)) { + return false; + } + + return true; } -function _mapAdUnitPathToElementId(adUnitCode) { - if (utils.isGptPubadsDefined()) { - // eslint-disable-next-line no-undef - const adSlots = googletag.pubads().getSlots(); - const isMatchingAdSlot = utils.isSlotMatchingAdUnitCode(adUnitCode); +function _validateVideo(bid) { + const videoAdUnit = utils.deepAccess(bid, 'mediaTypes.video'); + const videoBidderParams = utils.deepAccess(bid, 'params.video', {}); - for (let i = 0; i < adSlots.length; i++) { - if (isMatchingAdSlot(adSlots[i])) { - const id = adSlots[i].getSlotElementId(); + // If there's no video no need to validate against video rules + if (videoAdUnit === undefined) { + return true; + } - utils.logInfo(`[33Across Adapter] Map ad unit path to HTML element id: '${adUnitCode}' -> ${id}`); + if (!Array.isArray(videoAdUnit.playerSize)) { + return false; + } - return id; - } - } + if (!videoAdUnit.context) { + return false; } - utils.logWarn(`[33Across Adapter] Unable to locate element for ad unit code: '${adUnitCode}'`); + const videoParams = { + ...videoAdUnit, + ...videoBidderParams + }; - return null; + if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { + return false; + } + + if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { + return false; + } + + // If placement if defined, it must be a number + if ( + typeof videoParams.placement !== 'undefined' && + typeof videoParams.placement !== 'number' + ) { + return false; + } + + // If startdelay is defined it must be a number + if ( + videoAdUnit.context === 'instream' && + typeof videoParams.startdelay !== 'undefined' && + typeof videoParams.startdelay !== 'number' + ) { + return false; + } + + return true; } -function _getAdSlotHTMLElement(adUnitCode) { - return document.getElementById(adUnitCode) || - document.getElementById(_mapAdUnitPathToElementId(adUnitCode)); +// **************************** BUILD REQUESTS *************************** // +// NOTE: With regards to gdrp consent data, the server will independently +// infer the gdpr applicability therefore, setting the default value to false +function buildRequests(bidRequests, bidderRequest) { + const gdprConsent = Object.assign({ + consentString: undefined, + gdprApplies: false + }, bidderRequest && bidderRequest.gdprConsent); + + const uspConsent = bidderRequest && bidderRequest.uspConsent; + const pageUrl = (bidderRequest && bidderRequest.refererInfo) ? (bidderRequest.refererInfo.referer) : (undefined); + + adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(utils.uniques); + + return bidRequests.map(bidRequest => _createServerRequest( + { + bidRequest, + gdprConsent, + uspConsent, + pageUrl + }) + ); } // Infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request @@ -72,46 +162,28 @@ function _getAdSlotHTMLElement(adUnitCode) { function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl}) { const ttxRequest = {}; const params = bidRequest.params; - const element = _getAdSlotHTMLElement(bidRequest.adUnitCode); - const sizes = _transformSizes(bidRequest.sizes); - let format; - - // We support size based bidfloors so obtain one if there's a rule associated - if (typeof bidRequest.getFloor === 'function') { - let getFloor = bidRequest.getFloor.bind(bidRequest); - - format = sizes.map((size) => { - const formatExt = _getBidFloors(getFloor, size); + /* + * Infer data for the request payload + */ + ttxRequest.imp = [{}]; - return Object.assign({}, size, formatExt); - }); - } else { - format = sizes; + if (utils.deepAccess(bidRequest, 'mediaTypes.banner')) { + ttxRequest.imp[0].banner = { + ..._buildBannerORTB(bidRequest) + } } - const minSize = _getMinSize(sizes); - - const viewabilityAmount = _isViewabilityMeasurable(element) - ? _getViewability(element, utils.getWindowTop(), minSize) - : NON_MEASURABLE; - - const contributeViewability = ViewabilityContributor(viewabilityAmount); + if (utils.deepAccess(bidRequest, 'mediaTypes.video')) { + ttxRequest.imp[0].video = _buildVideoORTB(bidRequest); + } - /* - * Infer data for the request payload - */ - ttxRequest.imp = []; - ttxRequest.imp[0] = { - banner: { - format - }, - ext: { - ttx: { - prod: params.productId - } + ttxRequest.imp[0].ext = { + ttx: { + prod: _getProduct(bidRequest) } }; + ttxRequest.site = { id: params.siteId }; if (pageUrl) { @@ -173,53 +245,187 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl return { 'method': 'POST', 'url': url, - 'data': JSON.stringify(contributeViewability(ttxRequest)), + 'data': JSON.stringify(ttxRequest), 'options': options } } -// Sync object will always be of type iframe for TTX -function _createSync({ siteId = 'zzz000000000003zzz', gdprConsent = {}, uspConsent }) { - const ttxSettings = config.getConfig('ttxSettings'); - const syncUrl = (ttxSettings && ttxSettings.syncUrl) || SYNC_ENDPOINT; +// BUILD REQUESTS: SIZE INFERENCE +function _transformSizes(sizes) { + if (utils.isArray(sizes) && sizes.length === 2 && !utils.isArray(sizes[0])) { + return [ _getSize(sizes) ]; + } - const { consentString, gdprApplies } = gdprConsent; + return sizes.map(_getSize); +} - const sync = { - type: 'iframe', - url: `${syncUrl}&id=${siteId}&gdpr_consent=${encodeURIComponent(consentString)}&us_privacy=${encodeURIComponent(uspConsent)}` - }; +function _getSize(size) { + return { + w: parseInt(size[0], 10), + h: parseInt(size[1], 10) + } +} - if (typeof gdprApplies === 'boolean') { - sync.url += `&gdpr=${Number(gdprApplies)}`; +// BUILD REQUESTS: PRODUCT INFERENCE +function _getProduct(bidRequest) { + const { params, mediaTypes } = bidRequest; + + const { banner, video } = mediaTypes; + + if ((video && !banner) && video.context === 'instream') { + return PRODUCT.INSTREAM; } - return sync; + return (params.productId === PRODUCT.INVIEW) ? (params.productId) : PRODUCT.SIAB; } -function _getBidFloors(getFloor, size) { - const bidFloors = getFloor({ +// BUILD REQUESTS: BANNER +function _buildBannerORTB(bidRequest) { + const bannerAdUnit = utils.deepAccess(bidRequest, 'mediaTypes.banner', {}); + const element = _getAdSlotHTMLElement(bidRequest.adUnitCode); + + const sizes = _transformSizes(bannerAdUnit.sizes); + + let format; + + // We support size based bidfloors so obtain one if there's a rule associated + if (typeof bidRequest.getFloor === 'function') { + format = sizes.map((size) => { + const bidfloors = _getBidFloors(bidRequest, size, BANNER); + + let formatExt; + if (bidfloors) { + formatExt = { + ext: { + ttx: { + bidfloors: [ bidfloors ] + } + } + } + } + + return Object.assign({}, size, formatExt); + }); + } else { + format = sizes; + } + + const minSize = _getMinSize(sizes); + + const viewabilityAmount = _isViewabilityMeasurable(element) + ? _getViewability(element, utils.getWindowTop(), minSize) + : NON_MEASURABLE; + + const ext = contributeViewability(viewabilityAmount); + + return { + format, + ext + } +} + +// BUILD REQUESTS: VIDEO +// eslint-disable-next-line no-unused-vars +function _buildVideoORTB(bidRequest) { + const videoAdUnit = utils.deepAccess(bidRequest, 'mediaTypes.video', {}); + const videoBidderParams = utils.deepAccess(bidRequest, 'params.video', {}); + + const videoParams = { + ...videoAdUnit, + ...videoBidderParams // Bidder Specific overrides + }; + + const video = {} + + const {w, h} = _getSize(videoParams.playerSize[0]); + video.w = w; + video.h = h; + + // Obtain all ORTB params related video from Ad Unit + VIDEO_ORTB_PARAMS.forEach((param) => { + if (videoParams.hasOwnProperty(param)) { + video[param] = videoParams[param]; + } + }); + + const product = _getProduct(bidRequest); + + // Placement Inference Rules: + // - If no placement is defined then default to 2 (In Banner) + // - If product is instream (for instream context) then override placement to 1 + video.placement = video.placement || 2; + + if (product === PRODUCT.INSTREAM) { + video.startdelay = video.startdelay || 0; + video.placement = 1; + }; + + // bidfloors + if (typeof bidRequest.getFloor === 'function') { + const bidfloors = _getBidFloors(bidRequest, {w: video.w, h: video.h}, VIDEO); + + if (bidfloors) { + Object.assign(video, { + ext: { + ttx: { + bidfloors: [ bidfloors ] + } + } + }); + } + } + return video; +} + +// BUILD REQUESTS: BIDFLOORS +function _getBidFloors(bidRequest, size, mediaType) { + const bidFloors = bidRequest.getFloor({ currency: CURRENCY, - mediaType: MEDIA_TYPE, + mediaType, size: [ size.w, size.h ] }); if (!isNaN(bidFloors.floor) && (bidFloors.currency === CURRENCY)) { - return { - ext: { - ttx: { - bidfloors: [ bidFloors.floor ] - } + return bidFloors.floor; + } +} + +// BUILD REQUESTS: VIEWABILITY +function _isViewabilityMeasurable(element) { + return !_isIframe() && element !== null; +} + +function _getViewability(element, topWin, { w, h } = {}) { + return topWin.document.visibilityState === 'visible' + ? _getPercentInView(element, topWin, { w, h }) + : 0; +} + +function _mapAdUnitPathToElementId(adUnitCode) { + if (utils.isGptPubadsDefined()) { + // eslint-disable-next-line no-undef + const adSlots = googletag.pubads().getSlots(); + const isMatchingAdSlot = utils.isSlotMatchingAdUnitCode(adUnitCode); + + for (let i = 0; i < adSlots.length; i++) { + if (isMatchingAdSlot(adSlots[i])) { + const id = adSlots[i].getSlotElementId(); + + utils.logInfo(`[33Across Adapter] Map ad unit path to HTML element id: '${adUnitCode}' -> ${id}`); + + return id; } } } + + utils.logWarn(`[33Across Adapter] Unable to locate element for ad unit code: '${adUnitCode}'`); + + return null; } -function _getSize(size) { - return { - w: parseInt(size[0], 10), - h: parseInt(size[1], 10) - } +function _getAdSlotHTMLElement(adUnitCode) { + return document.getElementById(adUnitCode) || + document.getElementById(_mapAdUnitPathToElementId(adUnitCode)); } function _getMinSize(sizes) { @@ -239,14 +445,6 @@ function _getBoundingBox(element, { w, h } = {}) { return { width, height, left, top, right, bottom }; } -function _transformSizes(sizes) { - if (utils.isArray(sizes) && sizes.length === 2 && !utils.isArray(sizes[0])) { - return [ _getSize(sizes) ]; - } - - return sizes.map(_getSize); -} - function _getIntersectionOfRects(rects) { const bbox = { left: rects[0].left, @@ -307,20 +505,16 @@ function _getPercentInView(element, topWin, { w, h } = {}) { /** * Viewability contribution to request.. */ -function ViewabilityContributor(viewabilityAmount) { - function contributeViewability(ttxRequest) { - const req = Object.assign({}, ttxRequest); - const imp = req.imp = req.imp.map(impItem => Object.assign({}, impItem)); - const banner = imp[0].banner = Object.assign({}, imp[0].banner); - const ext = banner.ext = Object.assign({}, banner.ext); - const ttx = ext.ttx = Object.assign({}, ext.ttx); - - ttx.viewability = { amount: isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount) }; +function contributeViewability(viewabilityAmount) { + const amount = isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount); - return req; - } - - return contributeViewability; + return { + ttx: { + viewability: { + amount + } + } + }; } function _isIframe() { @@ -331,42 +525,9 @@ function _isIframe() { } } -function isBidRequestValid(bid) { - if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { - return false; - } - - if (typeof bid.params.siteId === 'undefined' || typeof bid.params.productId === 'undefined') { - return false; - } - - return true; -} - -// NOTE: With regards to gdrp consent data, -// - the server independently infers gdpr applicability therefore, setting the default value to false -function buildRequests(bidRequests, bidderRequest) { - const gdprConsent = Object.assign({ - consentString: undefined, - gdprApplies: false - }, bidderRequest && bidderRequest.gdprConsent); - - const uspConsent = bidderRequest && bidderRequest.uspConsent; - const pageUrl = (bidderRequest && bidderRequest.refererInfo) ? (bidderRequest.refererInfo.referer) : (undefined); - - adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(utils.uniques); - - return bidRequests.map(bidRequest => _createServerRequest( - { - bidRequest, - gdprConsent, - uspConsent, - pageUrl - }) - ); -} - -// NOTE: At this point, the response from 33exchange will only ever contain one bid i.e. the highest bid +// **************************** INTERPRET RESPONSE ******************************** // +// NOTE: At this point, the response from 33exchange will only ever contain one bid +// i.e. the highest bid function interpretResponse(serverResponse, bidRequest) { const bidResponses = []; @@ -378,6 +539,36 @@ function interpretResponse(serverResponse, bidRequest) { return bidResponses; } +// All this assumes that only one bid is ever returned by ttx +function _createBidResponse(response) { + const bid = { + requestId: response.id, + bidderCode: BIDDER_CODE, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ad: response.seatbid[0].bid[0].adm, + ttl: response.seatbid[0].bid[0].ttl || 60, + creativeId: response.seatbid[0].bid[0].crid, + mediaType: utils.deepAccess(response.seatbid[0].bid[0], 'ext.ttx.mediaType', BANNER), + currency: response.cur, + netRevenue: true + } + + if (bid.mediaType === VIDEO) { + const vastType = utils.deepAccess(response.seatbid[0].bid[0], 'ext.ttx.vastType', 'xml'); + + if (vastType === 'xml') { + bid.vastXml = bid.ad; + } else { + bid.vastUrl = bid.ad; + } + } + + return bid; +} + +// **************************** USER SYNC *************************** // // Register one sync per unique guid so long as iframe is enable // Else no syncs // For logic on how we handle gdpr data see _createSyncs and module's unit tests @@ -395,11 +586,30 @@ function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { return syncUrls; } +// Sync object will always be of type iframe for TTX +function _createSync({ siteId = 'zzz000000000003zzz', gdprConsent = {}, uspConsent }) { + const ttxSettings = config.getConfig('ttxSettings'); + const syncUrl = (ttxSettings && ttxSettings.syncUrl) || SYNC_ENDPOINT; + + const { consentString, gdprApplies } = gdprConsent; + + const sync = { + type: 'iframe', + url: `${syncUrl}&id=${siteId}&gdpr_consent=${encodeURIComponent(consentString)}&us_privacy=${encodeURIComponent(uspConsent)}` + }; + + if (typeof gdprApplies === 'boolean') { + sync.url += `&gdpr=${Number(gdprApplies)}`; + } + + return sync; +} + export const spec = { NON_MEASURABLE, code: BIDDER_CODE, - + supportedMediaTypes: [ BANNER, VIDEO ], isBidRequestValid, buildRequests, interpretResponse, diff --git a/modules/33acrossBidAdapter.md b/modules/33acrossBidAdapter.md index c313f3b6e0b..c01c04251e5 100644 --- a/modules/33acrossBidAdapter.md +++ b/modules/33acrossBidAdapter.md @@ -10,23 +10,145 @@ Maintainer: headerbidding@33across.com Connects to 33Across's exchange for bids. -33Across bid adapter supports only Banner at present and follows MRA +33Across bid adapter supports Banner and Video at present and follows MRA # Sample Ad Unit: For Publishers +## Sample Banner only Ad Unit ``` var adUnits = [ { - code: '33across-hb-ad-123456-1', // ad slot HTML element ID - sizes: [ - [300, 250], - [728, 90] - ], - bids: [{ - bidder: '33across', - params: { - siteId: 'cxBE0qjUir6iopaKkGJozW', - productId: 'siab' + code: '33across-hb-ad-123456-1', // ad slot HTML element ID + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + } + } + bids: [{ + bidder: '33across', + params: { + siteId: 'sample33xGUID123456789', + productId: 'siab' + } + }] +} +``` + +## Sample Video only Ad Unit: Outstream +``` +var adUnits = [ +{ + code: '33across-hb-ad-123456-1', // ad slot HTML element ID + mediaTypes: { + video: { + playerSize: [300, 250], + context: 'outstream', + placement: 2 + ... // Aditional ORTB video params + } + }, + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + render: function (bid) { + adResponse = { + ad: { + video: { + content: bid.vastXml, + player_height: bid.playerHeight, + player_width: bid.playerWidth + } + } } - }] + // push to render queue because ANOutstreamVideo may not be loaded yet. + bid.renderer.push(() => { + ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, // target div id to render video. + adResponse: adResponse + }); + }); + } + }, + bids: [{ + bidder: '33across', + params: { + siteId: 'sample33xGUID123456789', + productId: 'siab' + } + }] +} +``` + +## Sample Multi-Format Ad Unit: Outstream +``` +var adUnits = [ +{ + code: '33across-hb-ad-123456-1', // ad slot HTML element ID + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + }, + video: { + playerSize: [300, 250], + context: 'outstream', + placement: 2 + ... // Aditional ORTB video params + } + }, + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + render: function (bid) { + adResponse = { + ad: { + video: { + content: bid.vastXml, + player_height: bid.playerHeight, + player_width: bid.playerWidth + } + } + } + // push to render queue because ANOutstreamVideo may not be loaded yet. + bid.renderer.push(() => { + ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, // target div id to render video. + adResponse: adResponse + }); + }); + } + }, + bids: [{ + bidder: '33across', + params: { + siteId: 'sample33xGUID123456789', + productId: 'siab' + } + }] +} +``` + +## Sample Video only Ad Unit: Instream +``` +var adUnits = [ +{ + code: '33across-hb-ad-123456-1', // ad slot HTML element ID + mediaTypes: { + video: { + playerSize: [300, 250], + context: 'intstream', + placement: 1 + ... // Aditional ORTB video params + } + } + bids: [{ + bidder: '33across', + params: { + siteId: 'sample33xGUID123456789', + productId: 'instream' + } + }] } ``` diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 65a35284d1b..cab233d5387 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -2,22 +2,27 @@ import find from 'core-js-pure/features/array/find.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { loadExternalScript } from '../src/adloader.js' +import { loadExternalScript } from '../src/adloader.js'; import JSEncrypt from 'jsencrypt/bin/jsencrypt.js'; import sha256 from 'crypto-js/sha256.js'; import { getStorageManager } from '../src/storageManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; +import { createEidsArray } from './userId/eids.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; +import { OUTSTREAM } from '../src/video.js'; export const BIDDER_CODE = 'adagio'; export const LOG_PREFIX = 'Adagio:'; -export const VERSION = '2.4.0'; +export const VERSION = '2.6.0'; export const FEATURES_VERSION = '1'; export const ENDPOINT = 'https://mp.4dex.io/prebid'; -export const SUPPORTED_MEDIA_TYPES = ['banner']; +export const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; export const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js'; export const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript'; export const GVLID = 617; export const storage = getStorageManager(GVLID, 'adagio'); +export const RENDERER_URL = 'https://script.4dex.io/outstream-player.js'; export const ADAGIO_PUBKEY = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9el0+OEn6fvEh1RdVHQu4cnT0 @@ -26,6 +31,35 @@ t0b0lsHN+W4n9kitS/DZ/xnxWK/9vxhv0ZtL1LL/rwR5Mup7rmJbNtDoNBw4TIGj pV6EP3MTLosuUEpLaQIDAQAB -----END PUBLIC KEY-----`; +// This provide a whitelist and a basic validation +// of OpenRTB 2.5 options used by the Adagio SSP. +// https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf +export const ORTB_VIDEO_PARAMS = { + 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), + 'minduration': (value) => utils.isInteger(value), + 'maxduration': (value) => utils.isInteger(value), + 'protocols': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].indexOf(v) !== -1), + 'w': (value) => utils.isInteger(value), + 'h': (value) => utils.isInteger(value), + 'startdelay': (value) => utils.isInteger(value), + 'placement': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5].indexOf(v) !== -1), + 'linearity': (value) => [1, 2].indexOf(value) !== -1, + 'skip': (value) => [0, 1].indexOf(value) !== -1, + 'skipmin': (value) => utils.isInteger(value), + 'skipafter': (value) => utils.isInteger(value), + 'sequence': (value) => utils.isInteger(value), + 'battr': (value) => Array.isArray(value) && value.every(v => Array.from({length: 17}, (_, i) => i + 1).indexOf(v) !== -1), + 'maxextended': (value) => utils.isInteger(value), + 'minbitrate': (value) => utils.isInteger(value), + 'maxbitrate': (value) => utils.isInteger(value), + 'boxingallowed': (value) => [0, 1].indexOf(value) !== -1, + 'playbackmethod': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6].indexOf(v) !== -1), + 'playbackend': (value) => [1, 2, 3].indexOf(value) !== -1, + 'delivery': (value) => [1, 2, 3].indexOf(value) !== -1, + 'pos': (value) => [0, 1, 2, 3, 4, 5, 6, 7].indexOf(value) !== -1, + 'api': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6].indexOf(v) !== -1) +}; + let currentWindow; export function adagioScriptFromLocalStorageCb(ls) { @@ -63,7 +97,7 @@ export function adagioScriptFromLocalStorageCb(ls) { export function getAdagioScript() { storage.getDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY, (ls) => { - internal.adagioScriptFromLocalStorageCb(ls) + internal.adagioScriptFromLocalStorageCb(ls); }); storage.localStorageIsEnabled(isValid => { @@ -334,7 +368,7 @@ function getOrAddAdagioAdUnit(adUnitCode) { w.ADAGIO = w.ADAGIO || {}; if (w.ADAGIO.adUnits[adUnitCode]) { - return w.ADAGIO.adUnits[adUnitCode] + return w.ADAGIO.adUnits[adUnitCode]; } return w.ADAGIO.adUnits[adUnitCode] = {}; @@ -434,7 +468,7 @@ function getElementFromTopWindow(element, currentWindow) { }; function autoDetectAdUnitElementId(adUnitCode) { - const autoDetectedAdUnit = utils.getGptSlotInfoForAdUnitCode(adUnitCode) + const autoDetectedAdUnit = utils.getGptSlotInfoForAdUnitCode(adUnitCode); let adUnitElementId = null; if (autoDetectedAdUnit && autoDetectedAdUnit.divId) { @@ -449,16 +483,16 @@ function autoDetectEnvironment() { let environment; switch (device) { case 2: - environment = 'desktop' + environment = 'desktop'; break; case 4: - environment = 'mobile' + environment = 'mobile'; break; case 5: - environment = 'tablet' + environment = 'tablet'; break; }; - return environment + return environment; }; function getFeatures(bidRequest, bidderRequest) { @@ -506,6 +540,21 @@ function getFeatures(bidRequest, bidderRequest) { return features; }; +function isRendererPreferredFromPublisher(bidRequest) { + // renderer defined at adUnit level + const adUnitRenderer = utils.deepAccess(bidRequest, 'renderer'); + const hasValidAdUnitRenderer = !!(adUnitRenderer && adUnitRenderer.url && adUnitRenderer.render); + + // renderer defined at adUnit.mediaTypes level + const mediaTypeRenderer = utils.deepAccess(bidRequest, 'mediaTypes.video.renderer'); + const hasValidMediaTypeRenderer = !!(mediaTypeRenderer && mediaTypeRenderer.url && mediaTypeRenderer.render); + + return !!( + (hasValidAdUnitRenderer && !(adUnitRenderer.backupOnly === true)) || + (hasValidMediaTypeRenderer && !(mediaTypeRenderer.backupOnly === true)) + ); +} + export const internal = { enqueue, getOrAddAdagioAdUnit, @@ -520,7 +569,8 @@ export const internal = { getRefererInfo, adagioScriptFromLocalStorageCb, getCurrentWindow, - canAccessTopWindow + canAccessTopWindow, + isRendererPreferredFromPublisher }; function _getGdprConsent(bidderRequest) { @@ -538,7 +588,7 @@ function _getGdprConsent(bidderRequest) { const consent = {}; if (apiVersion !== undefined) { - consent.apiVersion = apiVersion + consent.apiVersion = apiVersion; } if (consentString !== undefined) { @@ -572,6 +622,64 @@ function _getSchain(bidRequest) { } } +function _getEids(bidRequest) { + if (utils.deepAccess(bidRequest, 'userId')) { + return createEidsArray(bidRequest.userId); + } +} + +function _buildVideoBidRequest(bidRequest) { + const videoAdUnitParams = utils.deepAccess(bidRequest, 'mediaTypes.video', {}); + const videoBidderParams = utils.deepAccess(bidRequest, 'params.video', {}); + const computedParams = {}; + + // Special case for playerSize. + // Eeach props will be overrided if they are defined in config. + if (Array.isArray(videoAdUnitParams.playerSize)) { + const tempSize = (Array.isArray(videoAdUnitParams.playerSize[0])) ? videoAdUnitParams.playerSize[0] : videoAdUnitParams.playerSize; + computedParams.w = tempSize[0]; + computedParams.h = tempSize[1]; + } + + const videoParams = { + ...computedParams, + ...videoAdUnitParams, + ...videoBidderParams + }; + + if (videoParams.context && videoParams.context === OUTSTREAM) { + bidRequest.mediaTypes.video.playerName = (internal.isRendererPreferredFromPublisher(bidRequest)) ? 'other' : 'adagio'; + + if (bidRequest.mediaTypes.video.playerName === 'other') { + utils.logWarn(`${LOG_PREFIX} renderer.backupOnly has not been set. Adagio recommends to use its own player to get expected behavior.`); + } + } + + // Only whitelisted OpenRTB options need to be validated. + // Other options will still remain in the `mediaTypes.video` object + // sent in the ad-request, but will be ignored by the SSP. + Object.keys(ORTB_VIDEO_PARAMS).forEach(paramName => { + if (videoParams.hasOwnProperty(paramName)) { + if (ORTB_VIDEO_PARAMS[paramName](videoParams[paramName])) { + bidRequest.mediaTypes.video[paramName] = videoParams[paramName]; + } else { + delete bidRequest.mediaTypes.video[paramName]; + utils.logWarn(`${LOG_PREFIX} The OpenRTB video param ${paramName} has been skipped due to misformating. Please refer to OpenRTB 2.5 spec.`); + } + } + }); +} + +function _renderer(bid) { + bid.renderer.push(() => { + if (typeof window.ADAGIO.outstreamPlayer === 'function') { + window.ADAGIO.outstreamPlayer(bid); + } else { + utils.logError(`${LOG_PREFIX} Adagio outstream player is not defined`); + } + }); +} + export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -593,7 +701,7 @@ export const spec = { ...params, adUnitElementId, environment - } + }; const debugData = () => ({ action: 'pb-dbg', @@ -624,7 +732,7 @@ export const spec = { // Store adUnits config. // If an adUnitCode has already been stored, it will be replaced. w.ADAGIO = w.ADAGIO || {}; - w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits.filter((adUnit) => adUnit.code !== adUnitCode) + w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits.filter((adUnit) => adUnit.code !== adUnitCode); w.ADAGIO.pbjsAdUnits.push({ code: adUnitCode, mediaTypes: mediaTypes || {}, @@ -657,8 +765,14 @@ export const spec = { const uspConsent = _getUspConsent(bidderRequest) || {}; const coppa = _getCoppa(); const schain = _getSchain(validBidRequests[0]); + const eids = _getEids(validBidRequests[0]) || []; const adUnits = utils._map(validBidRequests, (bidRequest) => { bidRequest.features = internal.getFeatures(bidRequest, bidderRequest); + + if (utils.deepAccess(bidRequest, 'mediaTypes.video')) { + _buildVideoBidRequest(bidRequest); + } + return bidRequest; }); @@ -666,7 +780,7 @@ export const spec = { const groupedAdUnits = adUnits.reduce((groupedAdUnits, adUnit) => { adUnit.params.organizationId = adUnit.params.organizationId.toString(); - groupedAdUnits[adUnit.params.organizationId] = groupedAdUnits[adUnit.params.organizationId] || [] + groupedAdUnits[adUnit.params.organizationId] = groupedAdUnits[adUnit.params.organizationId] || []; groupedAdUnits[adUnit.params.organizationId].push(adUnit); return groupedAdUnits; @@ -691,6 +805,9 @@ export const spec = { ccpa: uspConsent }, schain: schain, + user: { + eids: eids + }, prebidVersion: '$prebid.version$', adapterVersion: VERSION, featuresVersion: FEATURES_VERSION @@ -698,7 +815,7 @@ export const spec = { options: { contentType: 'text/plain' } - } + }; }); return requests; @@ -719,7 +836,30 @@ export const spec = { if (response.bids) { response.bids.forEach(bidObj => { const bidReq = (find(bidRequest.data.adUnits, bid => bid.bidId === bidObj.requestId)); + if (bidReq) { + if (bidObj.mediaType === VIDEO) { + const mediaTypeContext = utils.deepAccess(bidReq, 'mediaTypes.video.context'); + // Adagio SSP returns a `vastXml` only. No `vastUrl` nor `videoCacheKey`. + if (!bidObj.vastUrl && bidObj.vastXml) { + bidObj.vastUrl = 'data:text/xml;charset=utf-8;base64,' + btoa(bidObj.vastXml.replace(/\\"/g, '"')); + } + + if (mediaTypeContext === OUTSTREAM) { + bidObj.renderer = Renderer.install({ + id: bidObj.requestId, + adUnitCode: bidObj.adUnitCode, + url: bidObj.urlRenderer || RENDERER_URL, + config: { + ...utils.deepAccess(bidReq, 'mediaTypes.video'), + ...utils.deepAccess(bidObj, 'outstream', {}) + } + }); + + bidObj.renderer.setRender(_renderer); + } + } + bidObj.site = bidReq.params.site; bidObj.placement = bidReq.params.placement; bidObj.pagetype = bidReq.params.pagetype; diff --git a/modules/adagioBidAdapter.md b/modules/adagioBidAdapter.md index b34cc3fe37a..c55a24f1115 100644 --- a/modules/adagioBidAdapter.md +++ b/modules/adagioBidAdapter.md @@ -22,10 +22,10 @@ Connects to Adagio demand source to fetch bids. bids: [{ bidder: 'adagio', // Required params: { - organizationId: '0', // Required - Organization ID provided by Adagio. - site: 'news-of-the-day', // Required - Site Name provided by Adagio. - placement: 'ban_atf', // Required. Refers to the placement of an adunit in a page. Must not contain any information about the type of device. Other example: `mpu_btf'. - adUnitElementId: 'dfp_banniere_atf', // Required - AdUnit element id. Refers to the adunit id in a page. Usually equals to the adunit code above. + organizationId: '1002', // Required - Organization ID provided by Adagio. + site: 'adagio-io', // Required - Site Name provided by Adagio. + placement: 'in_article', // Required. Refers to the placement of an adunit in a page. Must not contain any information about the type of device. Other example: `mpu_btf'. + adUnitElementId: 'article_outstream', // Required - AdUnit element id. Refers to the adunit id in a page. Usually equals to the adunit code above. // The following params are limited to 30 characters, // and can only contain the following characters: @@ -37,7 +37,54 @@ Connects to Adagio demand source to fetch bids. environment: 'mobile', // Recommended. Environment where the page is displayed. category: 'sport', // Recommended. Category of the content displayed in the page. subcategory: 'handball', // Optional. Subcategory of the content displayed in the page. - postBid: false // Optional. Use it in case of Post-bid integration only. + postBid: false, // Optional. Use it in case of Post-bid integration only. + // Optional debug mode, used to get a bid response with expected cpm. + debug: { + enabled: true, + cpm: 3.00 // default to 1.00 + } + } + }] + }, + { + code: 'article_outstream', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + mimes: ['video/mp4'], + skip: 1 + // Other OpenRTB 2.5 video options… + } + }, + bids: [{ + bidder: 'adagio', // Required + params: { + organizationId: '1002', // Required - Organization ID provided by Adagio. + site: 'adagio-io', // Required - Site Name provided by Adagio. + placement: 'in_article', // Required. Refers to the placement of an adunit in a page. Must not contain any information about the type of device. Other example: `mpu_btf'. + adUnitElementId: 'article_outstream', // Required - AdUnit element id. Refers to the adunit id in a page. Usually equals to the adunit code above. + + // The following params are limited to 30 characters, + // and can only contain the following characters: + // - alphanumeric (A-Z+a-z+0-9, case-insensitive) + // - dashes `-` + // - underscores `_` + // Also, each param can have at most 50 unique active values (case-insensitive). + pagetype: 'article', // Highly recommended. The pagetype describes what kind of content will be present in the page. + environment: 'mobile', // Recommended. Environment where the page is displayed. + category: 'sport', // Recommended. Category of the content displayed in the page. + subcategory: 'handball', // Optional. Subcategory of the content displayed in the page. + postBid: false, // Optional. Use it in case of Post-bid integration only. + video: { + skip: 0 + // OpenRTB 2.5 video options defined here override ones defined in mediaTypes. + }, + // Optional debug mode, used to get a bid response with expected cpm. + debug: { + enabled: true, + cpm: 3.00 // default to 1.00 + } } }] } @@ -88,5 +135,4 @@ Connects to Adagio demand source to fetch bids. ] } } - ``` diff --git a/modules/adformBidAdapter.js b/modules/adformBidAdapter.js index 48000e082b2..05e45a428d3 100644 --- a/modules/adformBidAdapter.js +++ b/modules/adformBidAdapter.js @@ -23,7 +23,7 @@ export const spec = { const eids = getEncodedEIDs(utils.deepAccess(validBidRequests, '0.userIdAsEids')); var request = []; - var globalParams = [ [ 'adxDomain', 'adx.adform.net' ], [ 'fd', 1 ], [ 'url', null ], [ 'tid', null ] ]; + var globalParams = [ [ 'adxDomain', 'adx.adform.net' ], [ 'fd', 1 ], [ 'url', null ], [ 'tid', null ], [ 'eids', eids ] ]; var bids = JSON.parse(JSON.stringify(validBidRequests)); var bidder = (bids[0] && bids[0].bidder) || BIDDER_CODE; for (i = 0, l = bids.length; i < l; i++) { @@ -65,10 +65,6 @@ export const spec = { request.push('us_privacy=' + bidderRequest.uspConsent); } - if (eids) { - request.push('eids=' + eids); - } - for (i = 1, l = globalParams.length; i < l; i++) { _key = globalParams[i][0]; _value = globalParams[i][1]; @@ -100,7 +96,7 @@ export const spec = { function getEncodedEIDs(eids) { if (utils.isArray(eids) && eids.length > 0) { const parsed = parseEIDs(eids); - return encodeURIComponent(btoa(JSON.stringify(parsed))); + return btoa(JSON.stringify(parsed)); } } diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 972dd696bf6..c34902eda46 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -52,7 +52,7 @@ const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => { export const spec = { code: 'adkernel', - aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon'], + aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon', 'andbeyond', 'adbite', 'houseofpubs'], supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** @@ -116,13 +116,10 @@ export const spec = { requestId: rtbBid.impid, cpm: rtbBid.price, creativeId: rtbBid.crid, - currency: 'USD', + currency: response.cur || 'USD', ttl: 360, netRevenue: true }; - if (rtbBid.dealid !== undefined) { - prBid.dealId = rtbBid.dealid; - } if ('banner' in imp) { prBid.mediaType = BANNER; prBid.width = rtbBid.w; @@ -137,6 +134,27 @@ export const spec = { prBid.mediaType = NATIVE; prBid.native = buildNativeAd(JSON.parse(rtbBid.adm)); } + if (utils.isStr(rtbBid.dealid)) { + prBid.dealId = rtbBid.dealid; + } + if (utils.isArray(rtbBid.adomain)) { + utils.deepSetValue(prBid, 'meta.advertiserDomains', rtbBid.adomain); + } + if (utils.isArray(rtbBid.cat)) { + utils.deepSetValue(prBid, 'meta.secondaryCatIds', rtbBid.cat); + } + if (utils.isPlainObject(rtbBid.ext)) { + if (utils.isNumber(rtbBid.ext.advertiser_id)) { + utils.deepSetValue(prBid, 'meta.advertiserId', rtbBid.ext.advertiser_id); + } + if (utils.isStr(rtbBid.ext.advertiser_name)) { + utils.deepSetValue(prBid, 'meta.advertiserName', rtbBid.ext.advertiser_name); + } + if (utils.isStr(rtbBid.ext.agency_name)) { + utils.deepSetValue(prBid, 'meta.agencyName', rtbBid.ext.agency_name); + } + } + return prBid; }); }, diff --git a/modules/admanBidAdapter.js b/modules/admanBidAdapter.js index 5dc3412ee66..2e4091e7a24 100644 --- a/modules/admanBidAdapter.js +++ b/modules/admanBidAdapter.js @@ -95,10 +95,21 @@ export const spec = { return response; }, - getUserSyncs: (syncOptions, serverResponses) => { + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncUrl = URL_SYNC + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr==0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } return [{ type: 'image', - url: URL_SYNC + url: syncUrl }]; } diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index b2f24cfa910..405dd81cc8c 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -3,7 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'admixer'; const ALIASES = ['go2net']; -const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.0.aspx'; +const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.1.aspx'; export const spec = { code: BIDDER_CODE, aliases: ALIASES, @@ -51,10 +51,9 @@ export const spec = { */ interpretResponse: function (serverResponse, bidRequest) { const bidResponses = []; - // loop through serverResponses { try { - serverResponse = serverResponse.body; - serverResponse.forEach((bidResponse) => { + const {body: {ads = []} = {}} = serverResponse; + ads.forEach((bidResponse) => { const bidResp = { requestId: bidResponse.bidId, cpm: bidResponse.cpm, @@ -66,6 +65,7 @@ export const spec = { netRevenue: bidResponse.netRevenue, currency: bidResponse.currency, vastUrl: bidResponse.vastUrl, + dealId: bidResponse.dealId, }; bidResponses.push(bidResp); }); @@ -73,6 +73,19 @@ export const spec = { utils.logError(e); } return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { + const pixels = []; + serverResponses.forEach(({body: {cm = {}} = {}}) => { + const {pixels: img = [], iframes: frm = []} = cm; + if (syncOptions.pixelEnabled) { + img.forEach((url) => pixels.push({type: 'image', url})); + } + if (syncOptions.iframeEnabled) { + frm.forEach((url) => pixels.push({type: 'iframe', url})); + } + }); + return pixels; } }; registerBidder(spec); diff --git a/modules/adotBidAdapter.js b/modules/adotBidAdapter.js index 3d0af864a31..54bd9156b48 100644 --- a/modules/adotBidAdapter.js +++ b/modules/adotBidAdapter.js @@ -3,6 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {isStr, isArray, isNumber, isPlainObject, isBoolean, logError, replaceAuctionPrice} from '../src/utils.js'; import find from 'core-js-pure/features/array/find.js'; +import { config } from '../src/config.js'; const ADAPTER_VERSION = 'v1.0.0'; const BID_METHOD = 'POST'; @@ -334,13 +335,17 @@ function generateSiteFromAdUnitContext(adUnitContext) { if (!adUnitContext || !adUnitContext.refererInfo) return null; const domain = extractSiteDomainFromURL(adUnitContext.refererInfo.referer); + const publisherId = config.getConfig('adot.publisherId'); if (!domain) return null; return { page: adUnitContext.refererInfo.referer, domain: domain, - name: domain + name: domain, + publisher: { + id: publisherId + } }; } diff --git a/modules/adotBidAdapter.md b/modules/adotBidAdapter.md index 7fc1d84ee60..e1388311e23 100644 --- a/modules/adotBidAdapter.md +++ b/modules/adotBidAdapter.md @@ -214,3 +214,20 @@ const adUnit = { }] } ``` + +### PublisherId + +You can set a publisherId using `pbjs.setBidderConfig` for the bidder `adot` + +#### Example + +```javascript +pbjs.setBidderConfig({ + bidders: ['adot'], + config: { + adot: { + publisherId: '__MY_PUBLISHER_ID__' + } + } +}); +``` \ No newline at end of file diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index 2e9529b633c..a1fa202c154 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -6,16 +6,28 @@ import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'amx'; const storage = getStorageManager(737, BIDDER_CODE); -const SIMPLE_TLD_TEST = /\.co\.\w{2,4}$/; +const SIMPLE_TLD_TEST = /\.com?\.\w{2,4}$/; const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; -const VERSION = 'pba1.2'; +const VERSION = 'pba1.2.1'; const xmlDTDRxp = /^\s*<\?xml[^\?]+\?>/; const VAST_RXP = /^\s*<\??(?:vast|xml)/i; const TRACKING_ENDPOINT = 'https://1x1.a-mo.net/hbx/'; const AMUID_KEY = '__amuidpb'; -const getLocation = (request) => - parseUrl(deepAccess(request, 'refererInfo.canonicalUrl', location.href)) +function getLocation (request) { + const refInfo = request.refererInfo; + if (refInfo == null) { + return parseUrl(location.href); + } + + if (refInfo.isAmp && refInfo.referer != null) { + return parseUrl(refInfo.referer) + } + + const topUrl = refInfo.numIframes > 0 && refInfo.stack[0] != null + ? refInfo.stack[0] : location.href; + return parseUrl(topUrl); +}; const largestSize = (sizes, mediaTypes) => { const allSizes = sizes @@ -44,7 +56,7 @@ const nullOrType = (value, type) => function getID(loc) { const host = loc.hostname.split('.'); const short = host.slice( - host.length - (SIMPLE_TLD_TEST.test(loc.host) ? 3 : 2) + host.length - (SIMPLE_TLD_TEST.test(loc.hostname) ? 3 : 2) ).join('.'); return btoa(short).replace(/=+$/, ''); } @@ -239,7 +251,7 @@ export const spec = { gs: deepAccess(bidderRequest, 'gdprConsent.gdprApplies', ''), gc: deepAccess(bidderRequest, 'gdprConsent.consentString', ''), u: deepAccess(bidderRequest, 'refererInfo.canonicalUrl', loc.href), - do: loc.host, + do: loc.hostname, re: deepAccess(bidderRequest, 'refererInfo.referer'), am: getUIDSafe(), usp: bidderRequest.uspConsent || '1---', diff --git a/modules/andbeyondBidAdapter.md b/modules/andbeyondBidAdapter.md deleted file mode 100644 index 7d58bac0abc..00000000000 --- a/modules/andbeyondBidAdapter.md +++ /dev/null @@ -1,32 +0,0 @@ -# Overview - -``` -Module Name: andbeyond Bidder Adapter -Module Type: Bidder Adapter -Maintainer: shreyanschopra@rtbdemand.com -``` - -# Description - -Connects to andbeyond whitelabel platform. -Banner formats are supported. - - -# Test Parameters -``` - var adUnits = [ - { - code: 'banner-ad-div', - sizes: [[300, 250]], // banner size - bids: [ - { - bidder: 'andbeyond', - params: { - zoneId: '30164', //required parameter - host: 'cpm.metaadserving.com' //required parameter - } - } - ] - } - ]; -``` diff --git a/modules/aolBidAdapter.js b/modules/aolBidAdapter.js index 1f43231e495..c899da32340 100644 --- a/modules/aolBidAdapter.js +++ b/modules/aolBidAdapter.js @@ -30,6 +30,17 @@ const SYNC_TYPES = { } }; +const SUPPORTED_USER_ID_SOURCES = [ + 'adserver.org', + 'criteo.com', + 'id5-sync.com', + 'intentiq.com', + 'liveintent.com', + 'quantcast.com', + 'verizonmedia.com', + 'liveramp.com' +]; + const pubapiTemplate = template`${'host'}/pubapi/3.0/${'network'}/${'placement'}/${'pageid'}/${'sizeid'}/ADTECH;v=2;cmd=bid;cors=yes;alias=${'alias'};misc=${'misc'};${'dynamicParams'}`; const nexageBaseApiTemplate = template`${'host'}/bidRequest?`; const nexageGetApiTemplate = template`dcn=${'dcn'}&pos=${'pos'}&cmd=bid${'dynamicParams'}`; @@ -103,6 +114,12 @@ function resolveEndpointCode(bid) { } } +function getSupportedEids(bid) { + return bid.userIdAsEids.filter(eid => { + return SUPPORTED_USER_ID_SOURCES.indexOf(eid.source) !== -1 + }); +} + export const spec = { code: AOL_BIDDERS_CODES.AOL, gvlid: 25, @@ -226,6 +243,13 @@ export const spec = { }, buildOneMobileGetUrl(bid, consentData) { let { dcn, pos, ext } = bid.params; + if (typeof bid.userId === 'object') { + ext = ext || {}; + let eids = getSupportedEids(bid); + eids.forEach(eid => { + ext['eid' + eid.source] = eid.uids[0].id; + }); + } let nexageApi = this.buildOneMobileBaseUrl(bid); if (dcn && pos) { let dynamicParams = this.formatOneMobileDynamicParams(ext, consentData); @@ -292,6 +316,16 @@ export const spec = { utils.deepSetValue(openRtbObject, 'regs.ext.us_privacy', consentData.uspConsent); } + if (typeof bid.userId === 'object') { + openRtbObject.user = openRtbObject.user || {}; + openRtbObject.user.ext = openRtbObject.user.ext || {}; + + let eids = getSupportedEids(bid); + if (eids.length > 0) { + openRtbObject.user.ext.eids = eids + } + } + return openRtbObject; }, isEUConsentRequired(consentData) { diff --git a/modules/quantumdexBidAdapter.js b/modules/apacdexBidAdapter.js similarity index 94% rename from modules/quantumdexBidAdapter.js rename to modules/apacdexBidAdapter.js index 738b6165f79..2582e4788c1 100644 --- a/modules/quantumdexBidAdapter.js +++ b/modules/apacdexBidAdapter.js @@ -1,7 +1,11 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -const BIDDER_CODE = 'quantumdex'; +const BIDDER_CODE = 'apacdex'; const CONFIG = { + 'apacdex': { + 'ENDPOINT': 'https://useast.quantumdex.io/auction/apacdex', + 'USERSYNC': 'https://sync.quantumdex.io/usersync/apacdex' + }, 'quantumdex': { 'ENDPOINT': 'https://useast.quantumdex.io/auction/quantumdex', 'USERSYNC': 'https://sync.quantumdex.io/usersync/quantumdex' @@ -12,14 +16,14 @@ const CONFIG = { } }; -var bidderConfig = CONFIG['quantumdex']; +var bidderConfig = CONFIG[BIDDER_CODE]; var bySlotTargetKey = {}; var bySlotSizesCount = {} export const spec = { code: BIDDER_CODE, supportedMediaTypes: ['banner', 'video'], - aliases: ['valueimpression'], + aliases: ['quantumdex', 'valueimpression'], isBidRequestValid: function (bid) { if (!bid.params) { return false; @@ -30,7 +34,7 @@ export const spec = { if (!utils.deepAccess(bid, 'mediaTypes.banner') && !utils.deepAccess(bid, 'mediaTypes.video')) { return false; } - if (utils.deepAccess(bid, 'mediaTypes.banner')) { // Quantumdex does not support multi type bids, favor banner over video + if (utils.deepAccess(bid, 'mediaTypes.banner')) { // Not support multi type bids, favor banner over video if (!utils.deepAccess(bid, 'mediaTypes.banner.sizes')) { // sizes at the banner is required. return false; diff --git a/modules/quantumdexBidAdapter.md b/modules/apacdexBidAdapter.md similarity index 56% rename from modules/quantumdexBidAdapter.md rename to modules/apacdexBidAdapter.md index 8c35ea8cb05..b88190cda94 100644 --- a/modules/quantumdexBidAdapter.md +++ b/modules/apacdexBidAdapter.md @@ -1,15 +1,15 @@ # Overview ``` -Module Name: Quantum Digital Exchange Bidder Adapter +Module Name: APAC Digital Exchange Bidder Adapter Module Type: Bidder Adapter -Maintainer: ken@quantumdex.io +Maintainer: ken@apacdex.com ``` # Description -Connects to Quantum Digital Exchange for bids. -Quantumdex bid adapter supports Banner and Video (Instream and Outstream) ads. +Connects to APAC Digital Exchange for bids. +Apacdex bid adapter supports Banner and Video (Instream and Outstream) ads. # Test Parameters ``` @@ -23,9 +23,9 @@ var adUnits = [ }, bids: [ { - bidder: 'quantumdex', + bidder: 'apacdex', params: { - siteId: 'quantumdex-site-id', // siteId provided by Quantumdex + siteId: 'apacdex1234', // siteId provided by Apacdex } } ] @@ -46,9 +46,9 @@ var videoAdUnit = { }, bids: [ { - bidder: 'quantumdex', + bidder: 'apacdex', params: { - siteId: 'quantumdex-site-id', // siteId provided by Quantumdex + siteId: 'apacdex1234', // siteId provided by Apacdex } } ] diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index ff0e3230007..203835db611 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -229,16 +229,14 @@ export const spec = { } const criteoId = utils.deepAccess(bidRequests[0], `userId.criteoId`); + let eids = []; if (criteoId) { - let tpuids = []; - tpuids.push({ - 'provider': 'criteo', - 'user_id': criteoId + eids.push({ + source: 'criteo.com', + id: criteoId }); - payload.tpuids = tpuids; } - let eids = []; const tdid = utils.deepAccess(bidRequests[0], `userId.tdid`); if (tdid) { eids.push({ diff --git a/modules/appnexusBidAdapter.md b/modules/appnexusBidAdapter.md index 6ec40e83b41..d1f61836297 100644 --- a/modules/appnexusBidAdapter.md +++ b/modules/appnexusBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Appnexus Bid Adapter Module Type: Bidder Adapter -Maintainer: info@prebid.org +Maintainer: prebid-js@xandr.com ``` # Description diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js deleted file mode 100644 index 0f32c84962f..00000000000 --- a/modules/audigentRtdProvider.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * This module adds audigent provider to the real time data module - * The {@link module:modules/realTimeData} module is required - * The module will fetch segments from audigent server - * @module modules/audigentRtdProvider - * @requires module:modules/realTimeData - */ - -/** - * @typedef {Object} ModuleParams - * @property {string} siteKey - * @property {string} pubKey - * @property {string} url - * @property {?string} keyName - * @property {number} auctionDelay - */ - -import {config} from '../src/config.js'; -import {getGlobal} from '../src/prebidGlobal.js'; -import * as utils from '../src/utils.js'; -import {submodule} from '../src/hook.js'; -import {ajax} from '../src/ajax.js'; -import { getStorageManager } from '../src/storageManager.js'; - -const storage = getStorageManager(); - -/** @type {string} */ -const MODULE_NAME = 'realTimeData'; - -/** @type {ModuleParams} */ -let _moduleParams = {}; - -/** - * XMLHttpRequest to get data form audigent server - * @param {string} url server url with query params - */ - -export function setData(data) { - storage.setDataInLocalStorage('__adgntseg', JSON.stringify(data)); -} - -function getSegments(adUnits, onDone) { - try { - let jsonData = storage.getDataFromLocalStorage('__adgntseg'); - if (jsonData) { - let data = JSON.parse(jsonData); - if (data.audigent_segments) { - let dataToReturn = adUnits.reduce((rp, cau) => { - const adUnitCode = cau && cau.code; - if (!adUnitCode) { return rp } - rp[adUnitCode] = data; - return rp; - }, {}); - - onDone(dataToReturn); - return; - } - } - getSegmentsAsync(adUnits, onDone); - } catch (e) { - getSegmentsAsync(adUnits, onDone); - } -} - -function getSegmentsAsync(adUnits, onDone) { - const userIds = (getGlobal()).getUserIds(); - let tdid = null; - - if (userIds && userIds['tdid']) { - tdid = userIds['tdid']; - } else { - onDone({}); - } - - const url = `https://seg.ad.gt/api/v1/rtb_segments?tdid=${tdid}`; - - ajax(url, { - success: function (response, req) { - if (req.status === 200) { - try { - const data = JSON.parse(response); - if (data && data.audigent_segments) { - setData(data); - let dataToReturn = adUnits.reduce((rp, cau) => { - const adUnitCode = cau && cau.code; - if (!adUnitCode) { return rp } - rp[adUnitCode] = data; - return rp; - }, {}); - - onDone(dataToReturn); - } else { - onDone({}); - } - } catch (err) { - utils.logError('unable to parse audigent segment data'); - onDone({}) - } - } else if (req.status === 204) { - // unrecognized site key - onDone({}); - } - }, - error: function () { - onDone({}); - utils.logError('unable to get audigent segment data'); - } - } - ); -} - -/** @type {RtdSubmodule} */ -export const audigentSubmodule = { - /** - * used to link submodule with realTimeData - * @type {string} - */ - name: 'audigent', - /** - * get data and send back to realTimeData module - * @function - * @param {adUnit[]} adUnits - * @param {function} onDone - */ - getData: getSegments -}; - -export function init(config) { - const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - try { - _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter(pr => pr.name && pr.name.toLowerCase() === 'audigent')[0].params; - _moduleParams.auctionDelay = realTimeData.auctionDelay; - } catch (e) { - _moduleParams = {}; - } - confListener(); - }); -} - -submodule('realTimeData', audigentSubmodule); -init(config); diff --git a/modules/audigentRtdProvider.md b/modules/audigentRtdProvider.md deleted file mode 100644 index 47bcbbbf951..00000000000 --- a/modules/audigentRtdProvider.md +++ /dev/null @@ -1,52 +0,0 @@ -Audigent is a next-generation data management platform and a first-of-a-kind -"data agency" containing some of the most exclusive content-consuming audiences -across desktop, mobile and social platforms. - -This real-time data module provides first-party Audigent segments that can be -attached to bid request objects destined for different SSPs in order to optimize -targeting. Audigent maintains a large database of first-party Tradedesk Unified -ID to third party segment mappings that can now be queried at bid-time. - -Usage: - -Compile the audigent RTD module into your Prebid build: - -`gulp build --modules=userId,unifiedIdSystem,rtdModule,audigentRtdProvider,rubiconBidAdapter` - -Audigent segments will then be attached to each bid request objects in -`bid.realTimeData.audigent_segments` - -The format of the segments is a per-SSP mapping: - -``` -{ - 'appnexus': ['anseg1', 'anseg2'], - 'google': ['gseg1', 'gseg2'] -} -``` - -If a given SSP's API backend supports segment fields, they can then be -attached prior to the bid request being sent: - -``` -pbjs.requestBids({bidsBackHandler: addAudigentSegments}); - -function addAudigentSegments() { - for (i = 0; i < adUnits.length; i++) { - let adUnit = adUnits[i]; - for (j = 0; j < adUnit.bids.length; j++) { - adUnit.bids[j].userId.lipb.segments = adUnit.bids[j].realTimeData.audigent_segments['rubicon']; - } - } -} -``` - -To view an example of the segments returned by Audigent's backends: - -`gulp serve --modules=userId,unifiedIdSystem,rtdModule,audigentRtdProvider,rubiconBidAdapter` - -and then point your browser at: - -`http://localhost:9999/integrationExamples/gpt/audigentSegments_example.html` - - diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 12e78c684ad..4b30f47e2cf 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -6,7 +6,7 @@ import { VIDEO, BANNER } from '../src/mediaTypes.js'; import find from 'core-js-pure/features/array/find.js'; import includes from 'core-js-pure/features/array/includes.js'; -const ADAPTER_VERSION = '1.11'; +const ADAPTER_VERSION = '1.14'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; @@ -14,9 +14,14 @@ export const VIDEO_ENDPOINT = 'https://reachms.bfmio.com/bid.json?exchange_id='; export const BANNER_ENDPOINT = 'https://display.bfmio.com/prebid_display'; export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; -export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'placement']; +export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'placement', 'skip', 'skipmin', 'skipafter']; export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; +export const SUPPORTED_USER_IDS = [ + { key: 'tdid', source: 'adserver.org', rtiPartner: 'TDID', queryParam: 'tdid' }, + { key: 'idl_env', source: 'liveramp.com', rtiPartner: 'idl', queryParam: 'idl' } +]; + let appId = ''; export const spec = { @@ -56,18 +61,17 @@ export const spec = { response = response.body; if (isVideoBid(bidRequest)) { - if (!response || !response.url || !response.bidPrice) { + if (!response || !response.bidPrice) { utils.logWarn(`No valid video bids from ${spec.code} bidder`); return []; } let sizes = getVideoSizes(bidRequest); let firstSize = getFirstSize(sizes); let context = utils.deepAccess(bidRequest, 'mediaTypes.video.context'); - return { + let responseType = getVideoBidParam(bidRequest, 'responseType') || 'both'; + let bidResponse = { requestId: bidRequest.bidId, bidderCode: spec.code, - vastUrl: response.url, - vastXml: response.vast, cpm: response.bidPrice, width: firstSize.w, height: firstSize.h, @@ -78,6 +82,16 @@ export const spec = { netRevenue: true, ttl: 300 }; + + if (responseType === 'nurl' || responseType === 'both') { + bidResponse.vastUrl = response.url; + } + + if (responseType === 'adm' || responseType === 'both') { + bidResponse.vastXml = response.vast; + } + + return bidResponse; } else { if (!response || !response.length) { utils.logWarn(`No valid banner bids from ${spec.code} bidder`); @@ -257,13 +271,43 @@ function getTopWindowReferrer() { } } +function getEids(bid) { + return SUPPORTED_USER_IDS + .map(getUserId(bid)) + .filter(x => x); +} + +function getUserId(bid) { + return ({ key, source, rtiPartner }) => { + let id = bid.userId && bid.userId[key]; + return id ? formatEid(id, source, rtiPartner) : null; + }; +} + +function formatEid(id, source, rtiPartner) { + return { + source, + uids: [{ + id, + ext: { rtiPartner } + }] + }; +} + function getVideoTargetingParams(bid) { - return Object.keys(Object(bid.params.video)) - .filter(param => includes(VIDEO_TARGETING, param)) - .reduce((obj, param) => { - obj[ param ] = bid.params.video[ param ]; - return obj; - }, {}); + const result = {}; + const excludeProps = ['playerSize', 'context', 'w', 'h']; + Object.keys(Object(bid.mediaTypes.video)) + .filter(key => !includes(excludeProps, key)) + .forEach(key => { + result[ key ] = bid.mediaTypes.video[ key ]; + }); + Object.keys(Object(bid.params.video)) + .filter(key => includes(VIDEO_TARGETING, key)) + .forEach(key => { + result[ key ] = bid.params.video[ key ]; + }); + return result; } function createVideoRequestData(bid, bidderRequest) { @@ -274,6 +318,7 @@ function createVideoRequestData(bid, bidderRequest) { let bidfloor = getVideoBidParam(bid, 'bidfloor'); let tagid = getVideoBidParam(bid, 'tagid'); let topLocation = getTopWindowLocation(bidderRequest); + let eids = getEids(bid); let payload = { isPrebid: true, appId: appId, @@ -322,16 +367,8 @@ function createVideoRequestData(bid, bidderRequest) { payload.user.ext.consent = consentString; } - if (bid.userId && bid.userId.tdid) { - payload.user.ext.eids = [{ - source: 'adserver.org', - uids: [{ - id: bid.userId.tdid, - ext: { - rtiPartner: 'TDID' - } - }] - }]; + if (eids.length > 0) { + payload.user.ext.eids = eids; } let connection = navigator.connection || navigator.webkitConnection; @@ -378,9 +415,12 @@ function createBannerRequestData(bids, bidderRequest) { payload.gdprConsent = consentString; } - if (bids[0] && bids[0].userId && bids[0].userId.tdid) { - payload.tdid = bids[0].userId.tdid; - } + SUPPORTED_USER_IDS.forEach(({ key, queryParam }) => { + let id = bids[0] && bids[0].userId && bids[0].userId[key]; + if (id) { + payload[queryParam] = id; + } + }); return payload; } diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index fb3fcdb8d89..0ed05717391 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -59,6 +59,10 @@ export const spec = { } } + if (i.schain) { + params.schain = encodeToBase64WebSafe(JSON.stringify(i.schain)); + } + if (refInfo && refInfo.referer) params.ref = refInfo.referer; if (gdprConsent) { @@ -166,6 +170,10 @@ function getTz() { return new Date().getTimezoneOffset(); } +function encodeToBase64WebSafe(string) { + return btoa(string).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + /* function get_pubdata(adds) { if (adds !== undefined && adds.pubdata !== undefined) { diff --git a/modules/bizzclickBidAdapter.js b/modules/bizzclickBidAdapter.js new file mode 100644 index 00000000000..80d2f6b5395 --- /dev/null +++ b/modules/bizzclickBidAdapter.js @@ -0,0 +1,307 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; +import {config} from '../src/config.js'; + +const BIDDER_CODE = 'bizzclick'; +const ACCOUNTID_MACROS = '[account_id]'; +const URL_ENDPOINT = `https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=${ACCOUNTID_MACROS}`; +const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; +const NATIVE_PARAMS = { + title: { + id: 0, + name: 'title' + }, + icon: { + id: 2, + type: 1, + name: 'img' + }, + image: { + id: 3, + type: 3, + name: 'img' + }, + sponsoredBy: { + id: 5, + name: 'data', + type: 1 + }, + body: { + id: 4, + name: 'data', + type: 2 + }, + cta: { + id: 1, + type: 12, + name: 'data' + } +}; +const NATIVE_VERSION = '1.2'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: (bid) => { + return Boolean(bid.params.accountId) && Boolean(bid.params.placementId) + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: (validBidRequests, bidderRequest) => { + if (validBidRequests && validBidRequests.length === 0) return [] + let accuontId = validBidRequests[0].params.accountId; + const endpointURL = URL_ENDPOINT.replace(ACCOUNTID_MACROS, accuontId); + + let winTop = window; + let location; + try { + location = new URL(bidderRequest.refererInfo.referer) + winTop = window.top; + } catch (e) { + location = winTop.location; + utils.logMessage(e); + }; + + let bids = []; + for (let bidRequest of validBidRequests) { + let impObject = prepareImpObject(bidRequest); + let data = { + id: bidRequest.bidId, + test: config.getConfig('debug') ? 1 : 0, + cur: ['USD'], + device: { + w: winTop.screen.width, + h: winTop.screen.height, + language: (navigator && navigator.language) ? navigator.language.indexOf('-') != -1 ? navigator.language.split('-')[0] : navigator.language : '', + }, + site: { + page: location.pathname, + host: location.host + }, + source: { + tid: bidRequest.transactionId + }, + tmax: bidRequest.timeout, + imp: [impObject], + }; + bids.push(data) + } + return { + method: 'POST', + url: endpointURL, + data: bids + }; + }, + + /** + * 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: (serverResponse) => { + if (!serverResponse || !serverResponse.body) return [] + let bizzclickResponse = serverResponse.body; + + let bids = []; + for (let response of bizzclickResponse) { + let mediaType = response.seatbid[0].bid[0].ext && response.seatbid[0].bid[0].ext.mediaType ? response.seatbid[0].bid[0].ext.mediaType : BANNER; + + let bid = { + requestId: response.id, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ttl: response.ttl || 1200, + currency: response.cur || 'USD', + netRevenue: true, + creativeId: response.seatbid[0].bid[0].crid, + dealId: response.seatbid[0].bid[0].dealid, + mediaType: mediaType + }; + + switch (mediaType) { + case VIDEO: + bid.vastXml = response.seatbid[0].bid[0].adm + bid.vastUrl = response.seatbid[0].bid[0].ext.vastUrl + break + case NATIVE: + bid.native = parseNative(response.seatbid[0].bid[0].adm) + break + default: + bid.ad = response.seatbid[0].bid[0].adm + } + + bids.push(bid); + } + + return bids; + }, +}; + +/** + * Determine type of request + * + * @param bidRequest + * @param type + * @returns {boolean} + */ +const checkRequestType = (bidRequest, type) => { + return (typeof utils.deepAccess(bidRequest, `mediaTypes.${type}`) !== 'undefined'); +} + +const parseNative = admObject => { + const { assets, link, imptrackers, jstracker } = admObject.native; + const result = { + clickUrl: link.url, + clickTrackers: link.clicktrackers || undefined, + impressionTrackers: imptrackers || undefined, + javascriptTrackers: jstracker ? [ jstracker ] : undefined + }; + assets.forEach(asset => { + const kind = NATIVE_ASSET_IDS[asset.id]; + const content = kind && asset[NATIVE_PARAMS[kind].name]; + if (content) { + result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; + } + }); + + return result; +} + +const prepareImpObject = (bidRequest) => { + let impObject = { + id: bidRequest.transactionId, + secure: 1, + ext: { + placementId: bidRequest.params.placementId + } + }; + if (checkRequestType(bidRequest, BANNER)) { + impObject.banner = addBannerParameters(bidRequest); + } + if (checkRequestType(bidRequest, VIDEO)) { + impObject.video = addVideoParameters(bidRequest); + } + if (checkRequestType(bidRequest, NATIVE)) { + impObject.native = { + ver: NATIVE_VERSION, + request: addNativeParameters(bidRequest) + }; + } + return impObject +}; + +const addNativeParameters = bidRequest => { + let impObject = { + id: bidRequest.transactionId, + ver: NATIVE_VERSION, + }; + + const assets = utils._map(bidRequest.mediaTypes.native, (bidParams, key) => { + const props = NATIVE_PARAMS[key]; + const asset = { + required: bidParams.required & 1, + }; + if (props) { + asset.id = props.id; + let wmin, hmin; + let aRatios = bidParams.aspect_ratios; + + if (aRatios && aRatios[0]) { + aRatios = aRatios[0]; + wmin = aRatios.min_width || 0; + hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; + } + + if (bidParams.sizes) { + const sizes = flatten(bidParams.sizes); + wmin = sizes[0]; + hmin = sizes[1]; + } + + asset[props.name] = {} + + if (bidParams.len) asset[props.name]['len'] = bidParams.len; + if (props.type) asset[props.name]['type'] = props.type; + if (wmin) asset[props.name]['wmin'] = wmin; + if (hmin) asset[props.name]['hmin'] = hmin; + + return asset; + } + }).filter(Boolean); + + impObject.assets = assets; + return impObject +} + +const addBannerParameters = (bidRequest) => { + let bannerObject = {}; + const size = parseSizes(bidRequest, 'banner'); + bannerObject.w = size[0]; + bannerObject.h = size[1]; + return bannerObject; +}; + +const parseSizes = (bid, mediaType) => { + let mediaTypes = bid.mediaTypes; + if (mediaType === 'video') { + let size = []; + if (mediaTypes.video && mediaTypes.video.w && mediaTypes.video.h) { + size = [ + mediaTypes.video.w, + mediaTypes.video.h + ]; + } else if (Array.isArray(utils.deepAccess(bid, 'mediaTypes.video.playerSize')) && bid.mediaTypes.video.playerSize.length === 1) { + size = bid.mediaTypes.video.playerSize[0]; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0 && Array.isArray(bid.sizes[0]) && bid.sizes[0].length > 1) { + size = bid.sizes[0]; + } + return size; + } + let sizes = []; + if (Array.isArray(mediaTypes.banner.sizes)) { + sizes = mediaTypes.banner.sizes[0]; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + sizes = bid.sizes + } else { + utils.logWarn('no sizes are setup or found'); + } + + return sizes +} + +const addVideoParameters = (bidRequest) => { + let videoObj = {}; + let supportParamsList = ['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'placement', 'skip', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity'] + + for (let param of supportParamsList) { + if (bidRequest.mediaTypes.video[param] !== undefined) { + videoObj[param] = bidRequest.mediaTypes.video[param]; + } + } + + const size = parseSizes(bidRequest, 'video'); + videoObj.w = size[0]; + videoObj.h = size[1]; + return videoObj; +} + +const flatten = arr => { + return [].concat(...arr); +} + +registerBidder(spec); diff --git a/modules/bizzclickBidAdapter.md b/modules/bizzclickBidAdapter.md index 7dfa458b34c..6fc1bebf546 100644 --- a/modules/bizzclickBidAdapter.md +++ b/modules/bizzclickBidAdapter.md @@ -14,14 +14,91 @@ Module that connects to BizzClick SSP demand sources ``` var adUnits = [{ code: 'placementId', - sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, bids: [{ bidder: 'bizzclick', params: { - placementId: 0, - type: 'banner' + placementId: 'hash', + accountId: 'accountId' } }] + }, + { + code: 'native_example', + // sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true, + len: 800 + }, + image: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + privacyLink: { + required: false + }, + body: { + required: true + }, + icon: { + required: true, + sizes: [50, 50] + } + } + + }, + bids: [ { + bidder: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' + } + }] + }, + { + code: 'video1', + sizes: [640,480], + mediaTypes: { video: { + minduration:0, + maxduration:999, + boxingallowed:1, + skip:0, + mimes:[ + 'application/javascript', + 'video/mp4' + ], + w:1920, + h:1080, + protocols:[ + 2 + ], + linearity:1, + api:[ + 1, + 2 + ] + } }, + bids: [ + { + bidder: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' } + } + ] + } ]; ``` \ No newline at end of file diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 3aff3c6aac6..4ee338e94cc 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -13,30 +13,21 @@ * @property {string} pubKey * @property {string} url * @property {?string} keyName - * @property {?number} auctionDelay - * @property {?number} timeout */ -import {config} from '../src/config.js'; import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajaxBuilder} from '../src/ajax.js'; import {loadExternalScript} from '../src/adloader.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import find from 'core-js-pure/features/array/find.js'; const storage = getStorageManager(); -/** @type {string} */ -const MODULE_NAME = 'realTimeData'; -/** @type {number} */ -const DEF_TIMEOUT = 1000; /** @type {ModuleParams} */ let _moduleParams = {}; /** @type {null|Object} */ -let _data = null; -/** @type {null | function} */ -let _dataReadyCallback = null; +let _predictionsData = null; /** @type {string} */ const DEF_KEYNAME = 'browsiViewability'; @@ -63,7 +54,7 @@ export function addBrowsiTag(data) { * collect required data from page * send data to browsi server to get predictions */ -function collectData() { +export function collectData() { const win = window.top; const doc = win.document; let browsiData = null; @@ -88,59 +79,33 @@ function collectData() { } export function setData(data) { - _data = data; - - if (typeof _dataReadyCallback === 'function') { - _dataReadyCallback(_data); - _dataReadyCallback = null; - } -} - -/** - * wait for data from server - * call callback when data is ready - * @param {function} callback - */ -function waitForData(callback) { - if (_data) { - _dataReadyCallback = null; - callback(_data); - } else { - _dataReadyCallback = callback; - } + _predictionsData = data; } -/** - * filter server data according to adUnits received - * call callback (onDone) when data is ready - * @param {adUnit[]} adUnits - * @param {function} onDone callback function - */ -function sendDataToModule(adUnits, onDone) { +function sendDataToModule(adUnitsCodes) { try { - waitForData(_predictionsData => { - 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) { - if (!isIdMatchingAdUnit(adSlot, predictionData.w)) { - return rp; - } - rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); + const _predictions = (_predictionsData && _predictionsData.p) || {}; + return adUnitsCodes.reduce((rp, adUnitCode) => { + 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) { + if (!isIdMatchingAdUnit(adSlot, predictionData.w)) { + return rp; } - return rp; - }, {}); - return onDone(dataToReturn); - }); + rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); + } + return rp; + }, {}); } catch (e) { - onDone({}); + return {}; } } @@ -231,7 +196,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); + let ajax = ajaxBuilder(); ajax(url, { @@ -283,38 +248,23 @@ export const browsiSubmodule = { /** * get data and send back to realTimeData module * @function - * @param {adUnit[]} adUnits - * @param {function} onDone + * @param {string[]} adUnitsCodes */ - getData: sendDataToModule, - init: init + getTargetingData: sendDataToModule, + init: init, }; -function init(config, gdpr, usp) { +function init(moduleConfig) { + _moduleParams = moduleConfig.params; + if (_moduleParams && _moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { + collectData(); + } else { + utils.logError('missing params for Browsi provider'); + } 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 || DEF_TIMEOUT; - } catch (e) { - _moduleParams = {}; - } - if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { - collectData(); - } else { - utils.logError('missing params for Browsi provider'); - } - }); -} - function registerSubModule() { submodule('realTimeData', browsiSubmodule); } registerSubModule(); -beforeInit(config); diff --git a/modules/browsiRtdProvider.md b/modules/browsiRtdProvider.md new file mode 100644 index 00000000000..0dd8c1d7609 --- /dev/null +++ b/modules/browsiRtdProvider.md @@ -0,0 +1,55 @@ +# Overview + +The Browsi RTD module provides viewability predictions for ad slots on the page. +To use this module, you’ll need to work with [Browsi](https://gobrowsi.com/) to get an account and receive instructions on how to set up your pages and ad server. + +# Configurations + +Compile the Browsi RTD Provider into your Prebid build: + +`gulp build --modules=browsiRtdProvider` + + +Configuration example for using RTD module with `browsi` provider +```javascript + pbjs.setConfig({ + "realTimeData": { + "auctionDelay": 1000, + dataProviders:[{ + "name": "browsi", + "waitForIt": "true" + "params": { + "url": "testUrl.com", + "siteKey": "testKey", + "pubKey": "testPub", + "keyName":"bv" + } + }] + } + }); +``` + +#Params + +Contact Browsi to get required params + +| param name | type |Scope | Description | +| :------------ | :------------ | :------- | :------- | +| url | string | required | Browsi server URL | +| siteKey | string | required | Site key | +| pubKey | string | required | Publisher key | +| keyName | string | optional | Ad unit targeting key | + + +#Output +`getTargetingData` function will return expected viewability prediction in the following structure: +```json +{ + "adUnitCode":{ + "browsiViewability":"0.6" + }, + "adUnitCode2":{ + "browsiViewability":"0.9" + } +} +``` diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index d153ddf9ee2..3eb75799705 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -42,7 +42,7 @@ export const spec = { debug: utils.debugTurnedOn(), uid: getUid(bidderRequest), optedOut: hasOptedOutOfPersonalization(), - adapterVersion: '1.1.0', + adapterVersion: '1.1.1', uspConsent: bidderRequest.uspConsent, gdprConsent: bidderRequest.gdprConsent } @@ -53,9 +53,12 @@ export const spec = { name: bidRequest.adUnitCode, bidId: bidRequest.bidId, transactionId: bidRequest.transactionId, - sizes: bidRequest.sizes, + sizes: bidRequest.params.sizes || bidRequest.sizes, partnerId: bidRequest.params.partnerId, - slotType: bidRequest.params.slotType + slotType: bidRequest.params.slotType, + adSlot: bidRequest.params.slot || bidRequest.adUnitCode, + placementId: bidRequest.params.placementId || '', + site: bidRequest.params.site || bidderRequest.refererInfo.referer } return slot; diff --git a/modules/concertBidAdapter.md b/modules/concertBidAdapter.md index faf774946d1..d8736082e5c 100644 --- a/modules/concertBidAdapter.md +++ b/modules/concertBidAdapter.md @@ -24,10 +24,14 @@ Module that connects to Concert demand sources { bidder: "concert", params: { - partnerId: 'test_partner' + partnerId: 'test_partner', + site: 'site_name', + placementId: 1234567, + slot: 'slot_name', + sizes: [[1030, 590]] } } ] } ]; -``` \ No newline at end of file +``` diff --git a/modules/consentManagement.js b/modules/consentManagement.js index f44fde0554d..67af2baf959 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -199,29 +199,51 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) { let apiName = (cmpVersion === 2) ? '__tcfapi' : '__cmp'; + let callId = Math.random() + ''; + let callName = `${apiName}Call`; + /* Setup up a __cmp function to do the postMessage and stash the callback. This function behaves (from the caller's perspective identicially to the in-frame __cmp call */ - window[apiName] = function (cmd, arg, callback) { - let callId = Math.random() + ''; - let callName = `${apiName}Call`; - let msg = { - [callName]: { - command: cmd, - parameter: arg, - callId: callId - } - }; - if (cmpVersion !== 1) msg[callName].version = cmpVersion; + if (cmpVersion === 2) { + window[apiName] = function (cmd, cmpVersion, callback, arg) { + let msg = { + [callName]: { + command: cmd, + version: cmpVersion, + parameter: arg, + callId: callId + } + }; - cmpCallbacks[callId] = callback; - cmpFrame.postMessage(msg, '*'); - } + cmpCallbacks[callId] = callback; + cmpFrame.postMessage(msg, '*'); + } - /** when we get the return message, call the stashed callback */ - window.addEventListener('message', readPostMessageResponse, false); + /** when we get the return message, call the stashed callback */ + window.addEventListener('message', readPostMessageResponse, false); - // call CMP - window[apiName](commandName, undefined, moduleCallback); + // call CMP + window[apiName](commandName, cmpVersion, moduleCallback); + } else { + window[apiName] = function (cmd, arg, callback) { + let msg = { + [callName]: { + command: cmd, + parameter: arg, + callId: callId + } + }; + + cmpCallbacks[callId] = callback; + cmpFrame.postMessage(msg, '*'); + } + + /** when we get the return message, call the stashed callback */ + window.addEventListener('message', readPostMessageResponse, false); + + // call CMP + window[apiName](commandName, undefined, moduleCallback); + } function readPostMessageResponse(event) { let cmpDataPkgName = `${apiName}Return`; @@ -451,7 +473,7 @@ export function resetConsentData() { export function setConsentConfig(config) { // if `config.gdpr` or `config.usp` exist, assume new config format. // else for backward compatability, just use `config` - config = config.gdpr || config.usp ? config.gdpr : config; + config = config && (config.gdpr || config.usp ? config.gdpr : config); if (!config || typeof config !== 'object') { utils.logWarn('consentManagement config not defined, exiting consent manager'); return; diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index e4d5c12eb46..3edacb41549 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -269,7 +269,7 @@ export function resetConsentData() { * @param {object} config required; consentManagementUSP module config settings; usp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ export function setConsentConfig(config) { - config = config.usp; + config = config && config.usp; if (!config || typeof config !== 'object') { utils.logWarn('consentManagement.usp config not defined, exiting usp consent manager'); return; diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index 83bc773cb30..ac26d34d529 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -17,19 +17,11 @@ export const storage = getStorageManager(gvlid, bidderCode); const bididStorageKey = 'cto_bidid'; const bundleStorageKey = 'cto_bundle'; -const cookieWriteableKey = 'cto_test_cookie'; const cookiesMaxAge = 13 * 30 * 24 * 60 * 60 * 1000; const pastDateString = new Date(0).toString(); const expirationString = new Date(utils.timestamp() + cookiesMaxAge).toString(); -function areCookiesWriteable() { - storage.setCookie(cookieWriteableKey, '1'); - const canWrite = storage.getCookie(cookieWriteableKey) === '1'; - storage.setCookie(cookieWriteableKey, '', pastDateString); - return canWrite; -} - function extractProtocolHost (url, returnOnlyHost = false) { const parsedUrl = utils.parseUrl(url, {noDecodeWholeURL: true}) return returnOnlyHost @@ -60,20 +52,22 @@ function getCriteoDataFromAllStorages() { } } -function buildCriteoUsersyncUrl(topUrl, domain, bundle, areCookiesWriteable, isPublishertagPresent, gdprString) { +function buildCriteoUsersyncUrl(topUrl, domain, bundle, areCookiesWriteable, isLocalStorageWritable, isPublishertagPresent, gdprString) { const url = 'https://gum.criteo.com/sid/json?origin=prebid' + `${topUrl ? '&topUrl=' + encodeURIComponent(topUrl) : ''}` + `${domain ? '&domain=' + encodeURIComponent(domain) : ''}` + `${bundle ? '&bundle=' + encodeURIComponent(bundle) : ''}` + `${gdprString ? '&gdprString=' + encodeURIComponent(gdprString) : ''}` + `${areCookiesWriteable ? '&cw=1' : ''}` + - `${isPublishertagPresent ? '&pbt=1' : ''}` + `${isPublishertagPresent ? '&pbt=1' : ''}` + + `${isLocalStorageWritable ? '&lsw=1' : ''}`; return url; } function callCriteoUserSync(parsedCriteoData, gdprString) { - const cw = areCookiesWriteable(); + const cw = storage.cookiesAreEnabled(); + const lsw = storage.localStorageIsEnabled(); const topUrl = extractProtocolHost(getRefererInfo().referer); const domain = extractProtocolHost(document.location.href, true); const isPublishertagPresent = typeof criteo_pubtag !== 'undefined'; // eslint-disable-line camelcase @@ -83,6 +77,7 @@ function callCriteoUserSync(parsedCriteoData, gdprString) { domain, parsedCriteoData.bundle, cw, + lsw, isPublishertagPresent, gdprString ); diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 554c44aa708..13677c90bae 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -8,6 +8,7 @@ import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, bui import { config } from '../src/config.js'; import { getHook, submodule } from '../src/hook.js'; import { auctionManager } from '../src/auctionManager.js'; +import { uspDataHandler } from '../src/adapterManager.js'; import events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; @@ -100,6 +101,9 @@ export function buildDfpVideoUrl(options) { const descriptionUrl = getDescriptionUrl(bid, options, 'params'); if (descriptionUrl) { queryParams.description_url = descriptionUrl; } + const uspConsent = uspDataHandler.getConsentData(); + if (uspConsent) { queryParams.us_privacy = uspConsent; } + return buildUrl({ protocol: 'https', host: 'securepubads.g.doubleclick.net', @@ -183,6 +187,9 @@ export function buildAdpodVideoUrl({code, params, callback} = {}) { { cust_params: encodedCustomParams } ); + const uspConsent = uspDataHandler.getConsentData(); + if (uspConsent) { queryParams.us_privacy = uspConsent; } + const masterTag = buildUrl({ protocol: 'https', host: 'securepubads.g.doubleclick.net', diff --git a/modules/districtmDMXBidAdapter.js b/modules/districtmDMXBidAdapter.js index bcb2bb97210..60f6b9b64b1 100644 --- a/modules/districtmDMXBidAdapter.js +++ b/modules/districtmDMXBidAdapter.js @@ -1,6 +1,6 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import {config} from '../src/config.js'; +import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'districtmDMX'; @@ -28,20 +28,22 @@ export const spec = { response = response.body || {}; if (response.seatbid) { if (utils.isArray(response.seatbid)) { - const {seatbid} = response; + const { seatbid } = response; let winners = seatbid.reduce((bid, ads) => { - let ad = ads.bid.reduce(function(oBid, nBid) { + let ad = ads.bid.reduce(function (oBid, nBid) { if (oBid.price < nBid.price) { const bid = matchRequest(nBid.impid, bidRequest); - const {width, height} = defaultSize(bid); + const { width, height } = defaultSize(bid); nBid.cpm = parseFloat(nBid.price).toFixed(2); nBid.bidId = nBid.impid; nBid.requestId = nBid.impid; nBid.width = nBid.w || width; nBid.height = nBid.h || height; - nBid.mediaType = bid.mediaTypes && bid.mediaTypes.video ? 'video' : null; + nBid.ttl = 360; + nBid.mediaType = bid.mediaTypes && bid.mediaTypes.video ? 'video' : 'banner'; if (nBid.mediaType) { - nBid.vastXml = cleanVast(nBid.adm); + nBid.vastXml = cleanVast(nBid.adm, nBid.nurl); + nBid.ttl = 3600; } if (nBid.dealid) { nBid.dealId = nBid.dealid; @@ -51,7 +53,6 @@ export const spec = { 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; @@ -61,7 +62,7 @@ export const spec = { oBid.cpm = oBid.price; return oBid; } - }, {price: 0}); + }, { price: 0 }); if (ad.adm) { bid.push(ad) } @@ -98,7 +99,7 @@ export const spec = { let params = config.getConfig('dmx'); dmxRequest.user = params.user || {}; let site = params.site || {}; - dmxRequest.site = {...dmxRequest.site, ...site} + dmxRequest.site = { ...dmxRequest.site, ...site } } catch (e) { } @@ -128,9 +129,12 @@ export const spec = { dmxRequest.regs = {}; dmxRequest.regs.ext = {}; dmxRequest.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies === true ? 1 : 0; - dmxRequest.user = {}; - dmxRequest.user.ext = {}; - dmxRequest.user.ext.consent = bidderRequest.gdprConsent.consentString; + + if (bidderRequest.gdprConsent.gdprApplies === true) { + dmxRequest.user = {}; + dmxRequest.user.ext = {}; + dmxRequest.user.ext.consent = bidderRequest.gdprConsent.consentString; + } } dmxRequest.regs = dmxRequest.regs || {}; dmxRequest.regs.coppa = config.getConfig('coppa') === true ? 1 : 0; @@ -144,7 +148,7 @@ export const spec = { dmxRequest.source = {}; dmxRequest.source.ext = {}; dmxRequest.source.ext.schain = schain || {} - } catch (e) {} + } catch (e) { } let tosendtags = bidRequest.map(dmx => { var obj = {}; obj.id = dmx.bidId; @@ -154,19 +158,16 @@ export const spec = { if (dmx.mediaTypes && dmx.mediaTypes.video) { obj.video = { topframe: 1, - skip: dmx.mediaTypes.video.skippable || 0, + skip: dmx.mediaTypes.video.skip || 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), + playbackmethod: dmx.mediaTypes.video.playbackmethod || [2], 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') + w: dmx.mediaTypes.video.playerSize[0][0] }; } else { obj.banner = { @@ -174,7 +175,7 @@ export const spec = { w: cleanSizes(dmx.sizes, 'w'), h: cleanSizes(dmx.sizes, 'h'), format: cleanSizes(dmx.sizes).map(s => { - return {w: s[0], h: s[1]}; + return { w: s[0], h: s[1] }; }).filter(obj => typeof obj.w === 'number' && typeof obj.h === 'number') }; } @@ -217,7 +218,7 @@ export const spec = { } } -export function getFloor (bid) { +export function getFloor(bid) { let floor = null; if (typeof bid.getFloor === 'function') { const floorInfo = bid.getFloor({ @@ -297,7 +298,7 @@ export function shuffle(sizes, list) { } results.push(current); results = list.filter(l => results.map(r => `${r[0]}x${r[1]}`).indexOf(`${l.size[0]}x${l.size[1]}`) !== -1); - results = results.sort(function(a, b) { + results = results.sort(function (a, b) { return b.s - a.s; }) return results.map(r => r.size); @@ -343,7 +344,7 @@ export function upto5(allimps, dmxRequest, bidderRequest, DMXURI) { * */ export function matchRequest(id, bidRequest) { - const {bids} = bidRequest.bidderRequest; + const { bids } = bidRequest.bidderRequest; const [returnValue] = bids.filter(bid => bid.bidId === id); return returnValue; } @@ -359,7 +360,7 @@ export function checkDeepArray(Arr) { } } export function defaultSize(thebidObj) { - const {sizes} = thebidObj; + const { sizes } = thebidObj; const returnObject = {}; returnObject.width = checkDeepArray(sizes)[0]; returnObject.height = checkDeepArray(sizes)[1]; @@ -380,20 +381,10 @@ export function bindUserId(eids, value, source, atype) { } } -export function getApi({protocols}) { +export function getApi({ api }) { 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) + if (api && Array.isArray(api) && api.length > 0) { + return api } else { return defaultValue; } @@ -407,37 +398,34 @@ export function getPlaybackmethod(playback) { return [2] } -export function getProtocols({protocols}) { +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); + if (protocols && Array.isArray(protocols) && protocols.length > 0) { + return protocols; } 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}`) +export function cleanVast(str, nurl) { + try { + 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; + } catch (e) { + if (!nurl) { + return str } + const insrt = `` + str = str.replace('', `${insrt}`) + return str } - return str; } registerBidder(spec); diff --git a/modules/emx_digitalBidAdapter.js b/modules/emx_digitalBidAdapter.js index fa58481548a..72da18d5691 100644 --- a/modules/emx_digitalBidAdapter.js +++ b/modules/emx_digitalBidAdapter.js @@ -156,6 +156,17 @@ export const emxAdapter = { }; } + return emxData; + }, + getSupplyChain: (bidRequests, emxData) => { + if (bidRequests.schain) { + emxData.source = { + ext: { + schain: bidRequests.schain + } + }; + } + return emxData; } }; @@ -237,6 +248,7 @@ export const spec = { }; emxData = emxAdapter.getGdpr(bidderRequest, Object.assign({}, emxData)); + emxData = emxAdapter.getSupplyChain(bidderRequest, Object.assign({}, emxData)); if (bidderRequest && bidderRequest.uspConsent) { emxData.us_privacy = bidderRequest.uspConsent } diff --git a/modules/etargetBidAdapter.js b/modules/etargetBidAdapter.js index 5e07561044a..42c991a17a4 100644 --- a/modules/etargetBidAdapter.js +++ b/modules/etargetBidAdapter.js @@ -96,6 +96,7 @@ export const spec = { currency: data.win_cur, netRevenue: true, ttl: 360, + reason: data.reason ? data.reason : 'none', ad: data.banner, vastXml: data.vast_content, vastUrl: data.vast_link, diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index dce678362cb..53f490a0a3c 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -407,11 +407,20 @@ export const spec = { return bidResponses; }, - getUserSyncs: function(syncOptions) { + getUserSyncs: function(syncOptions, responses, gdprConsent, usPrivacy) { + var gdprParams = ''; + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `?gdpr_consent=${gdprConsent.consentString}`; + } + } + if (syncOptions && syncOptions.pixelEnabled) { return [{ type: 'image', - url: USER_SYNC_URL + url: USER_SYNC_URL + gdprParams }]; } else { return []; diff --git a/modules/gamoshiBidAdapter.js b/modules/gamoshiBidAdapter.js index 2e09cf55d0a..48a142a66a6 100644 --- a/modules/gamoshiBidAdapter.js +++ b/modules/gamoshiBidAdapter.js @@ -42,7 +42,7 @@ export const helper = { export const spec = { code: 'gamoshi', - aliases: ['gambid', 'cleanmedia', '9MediaOnline'], + aliases: ['gambid', 'cleanmedia', '9MediaOnline', 'MobfoxX'], supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { diff --git a/modules/gamoshiBidAdapter.md b/modules/gamoshiBidAdapter.md index 6e930375059..49b727cecae 100644 --- a/modules/gamoshiBidAdapter.md +++ b/modules/gamoshiBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Gamoshi Bid Adapter Module Type: Bidder Adapter -Maintainer: salomon@gamoshi.com +Maintainer: dev@gamoshi.com ``` # Description diff --git a/modules/geoedgeRtdProvider.js b/modules/geoedgeRtdProvider.js new file mode 100644 index 00000000000..001ef67b66a --- /dev/null +++ b/modules/geoedgeRtdProvider.js @@ -0,0 +1,213 @@ +/** + * This module adds geoedge provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will fetch creative wrapper from geoedge server + * The module will place geoedge RUM client on bid responses markup + * @module modules/geoedgeProvider + * @requires module:modules/realTimeData + */ + +/** + * @typedef {Object} ModuleParams + * @property {string} key + * @property {?Object} bidders + * @property {?boolean} wap + * @property {?string} keyName + */ + +import { submodule } from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { generateUUID, insertElement, isEmpty, logError } from '../src/utils.js'; + +/** @type {string} */ +const SUBMODULE_NAME = 'geoedge'; +/** @type {string} */ +export const WRAPPER_URL = 'https://wrappers.geoedge.be/wrapper.html'; +/** @type {string} */ +/* eslint-disable no-template-curly-in-string */ +export const HTML_PLACEHOLDER = '${creative}'; +/** @type {string} */ +const PV_ID = generateUUID(); +/** @type {string} */ +const HOST_NAME = 'https://rumcdn.geoedge.be'; +/** @type {string} */ +const FILE_NAME = 'grumi.js'; +/** @type {function} */ +export let getClientUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME}`; +/** @type {string} */ +export let wrapper +/** @type {boolean} */; +let wrapperReady; +/** @type {boolean} */; +let preloaded; + +/** + * fetches the creative wrapper + * @param {function} sucess - success callback + */ +export function fetchWrapper(success) { + if (wrapperReady) { + return success(wrapper); + } + ajax(WRAPPER_URL, success); +} + +/** + * sets the wrapper and calls preload client + * @param {string} responseText + */ +export function setWrapper(responseText) { + wrapperReady = true; + wrapper = responseText; +} + +/** + * preloads the client + * @param {string} key + */ +export function preloadClient(key) { + let link = document.createElement('link'); + link.rel = 'preload'; + link.as = 'script'; + link.href = getClientUrl(key); + link.onload = () => { preloaded = true }; + insertElement(link); +} + +/** + * creates identity function for string replace without special replacement patterns + * @param {string} str + * @return {function} + */ +function replacer(str) { + return function () { + return str; + } +} + +export function wrapHtml(wrapper, html) { + return wrapper.replace(HTML_PLACEHOLDER, replacer(html)); +} + +/** + * generate macros dictionary from bid response + * @param {Object} bid + * @param {string} key + * @return {Object} + */ +function getMacros(bid, key) { + return { + '${key}': key, + '%%ADUNIT%%': bid.adUnitCode, + '%%WIDTH%%': bid.width, + '%%HEIGHT%%': bid.height, + '%%PATTERN:hb_adid%%': bid.adId, + '%%PATTERN:hb_bidder%%': bid.bidderCode, + '%_isHb!': true, + '%_hbcid!': bid.creativeId || '', + '%%PATTERN:hb_pb%%': bid.pbHg, + '%%SITE%%': location.hostname, + '%_pimp%': PV_ID + }; +} + +/** + * replace macro placeholders in a string with values from a dictionary + * @param {string} wrapper + * @param {Object} macros + * @return {string} + */ +function replaceMacros(wrapper, macros) { + var re = new RegExp('\\' + Object.keys(macros).join('|'), 'gi'); + + return wrapper.replace(re, function(matched) { + return macros[matched]; + }); +} + +/** + * build final creative html with creative wrapper + * @param {Object} bid + * @param {string} wrapper + * @param {string} html + * @return {string} + */ +function buildHtml(bid, wrapper, html, key) { + let macros = getMacros(bid, key); + wrapper = replaceMacros(wrapper, macros); + return wrapHtml(wrapper, html); +} + +/** + * muatates the bid ad property + * @param {Object} bid + * @param {string} ad + */ +function mutateBid(bid, ad) { + bid.ad = ad; +} + +/** + * wraps a bid object with the creative wrapper + * @param {Object} bid + * @param {string} key + */ +export function wrapBidResponse(bid, key) { + let wrapped = buildHtml(bid, wrapper, bid.ad, key); + mutateBid(bid, wrapped); +} + +/** + * checks if bidder's bids should be monitored + * @param {string} bidder + * @return {boolean} + */ +function isSupportedBidder(bidder, paramsBidders) { + return isEmpty(paramsBidders) || paramsBidders[bidder] === true; +} + +/** + * checks if bid should be monitored + * @param {Object} bid + * @return {boolean} + */ +function shouldWrap(bid, params) { + let supportedBidder = isSupportedBidder(bid.bidderCode, params.bidders); + let donePreload = params.wap ? preloaded : true; + return wrapperReady && supportedBidder && donePreload; +} + +function conditionallyWrap(bidResponse, config, userConsent) { + let params = config.params; + if (shouldWrap(bidResponse, params)) { + wrapBidResponse(bidResponse, params.key); + } +} + +function init(config, userConsent) { + let params = config.params; + if (!params || !params.key) { + logError('missing key for geoedge RTD module provider'); + return false; + } + preloadClient(params.key); + return true; +} + +/** @type {RtdSubmodule} */ +export const geoedgeSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: SUBMODULE_NAME, + init, + onBidResponseEvent: conditionallyWrap +}; + +export function beforeInit() { + fetchWrapper(setWrapper); + submodule('realTimeData', geoedgeSubmodule); +} + +beforeInit(); diff --git a/modules/geoedgeRtdProvider.md b/modules/geoedgeRtdProvider.md new file mode 100644 index 00000000000..e4aa046a97d --- /dev/null +++ b/modules/geoedgeRtdProvider.md @@ -0,0 +1,67 @@ +## Overview + +Module Name: Geoedge Rtd provider +Module Type: Rtd Provider +Maintainer: guy.books@geoedge.com + +The Geoedge Realtime module let pusblishers to block bad ads such as automatic redirects, malware, offensive creatives and landing pages. +To use this module, you'll need to work with [Geoedge](https://www.geoedge.com/publishers-real-time-protection/) to get an account and cutomer key. + +## Integration + +1) Build the geoedge RTD module into the Prebid.js package with: + +``` +gulp build --modules=geoedgeRtdProvider,... +``` + +2) Use `setConfig` to instruct Prebid.js to initilize the geoedge module, as specified below. + +## Configuration + +This module is configured as part of the `realTimeData.dataProviders` object: + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'geoedge', + params: { + key: '123123', + bidders: { + 'bidderA': true, // monitor bids form this bidder + 'bidderB': false // do not monitor bids form this bidder. + }, + wap: true + } + }] + } +}); +``` + +Parameters details: + +{: .table .table-bordered .table-striped } +|Name |Type |Description |Notes | +| :------------ | :------------ | :------------ |:------------ | +|name | String | Real time data module name |Required, always 'geoedge' | +|params | Object | | | +|params.key | String | Customer key |Required, contact Geoedge to get your key | +|params.bidders | Object | Bidders to monitor |Optional, list of bidder to include / exclude from monitoring. Omitting this will monitor bids from all bidders. | +|params.wap |Boolean |Wrap after preload |Optional, defaults to `false`. Set to `true` if you want to monitor only after the module has preloaded the monitoring client. | + +## Example + +To view an integration example: + +1) in your cli run: + +``` +gulp serve --modules=appnexusBidAdapter,geoedgeRtdProvider +``` + +2) in your browser, navigate to: + +``` +http://localhost:9999/integrationExamples/gpt/geoedgeRtdProvider_example.html +``` diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index 378fc5a7efe..3f9d4fba31d 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -5,8 +5,7 @@ import { VIDEO, BANNER } from '../src/mediaTypes.js'; import {config} from '../src/config.js'; const BIDDER_CODE = 'grid'; -const ENDPOINT_URL = 'https://grid.bidswitch.net/hb'; -const NEW_ENDPOINT_URL = 'https://grid.bidswitch.net/hbjson'; +const ENDPOINT_URL = 'https://grid.bidswitch.net/hbjson'; const SYNC_URL = 'https://x.bidswitch.net/sync?ssp=themediagrid'; const TIME_TO_LIVE = 360; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; @@ -44,19 +43,236 @@ export const spec = { * @return {ServerRequest[]} Info describing the request to the server. */ buildRequests: function(validBidRequests, bidderRequest) { - const oldFormatBids = []; - const newFormatBids = []; + if (!validBidRequests.length) { + return null; + } + let pageKeywords = null; + let jwpseg = null; + let content = null; + let schain = null; + let userId = null; + let user = null; + let userExt = null; + let {bidderRequestId, auctionId, gdprConsent, uspConsent, timeout, refererInfo} = bidderRequest || {}; + + const referer = refererInfo ? encodeURIComponent(refererInfo.referer) : ''; + const imp = []; + const bidsMap = {}; + validBidRequests.forEach((bid) => { - bid.params.useNewFormat ? newFormatBids.push(bid) : oldFormatBids.push(bid); + if (!bidderRequestId) { + bidderRequestId = bid.bidderRequestId; + } + if (!auctionId) { + auctionId = bid.auctionId; + } + if (!schain) { + schain = bid.schain; + } + if (!userId) { + userId = bid.userId; + } + const {params: {uid, keywords}, mediaTypes, bidId, adUnitCode, rtd} = bid; + bidsMap[bidId] = bid; + if (!pageKeywords && !utils.isEmpty(keywords)) { + pageKeywords = utils.transformBidderParamKeywords(keywords); + } + const bidFloor = _getFloor(mediaTypes || {}, bid); + const jwTargeting = rtd && rtd.jwplayer && rtd.jwplayer.targeting; + if (jwTargeting) { + if (!jwpseg && jwTargeting.segments) { + jwpseg = jwTargeting.segments; + } + if (!content && jwTargeting.content) { + content = jwTargeting.content; + } + } + let impObj = { + id: bidId, + tagid: uid.toString(), + ext: { + divid: adUnitCode + } + }; + + if (bidFloor) { + impObj.bidfloor = bidFloor; + } + + if (!mediaTypes || mediaTypes[BANNER]) { + const banner = createBannerRequest(bid, mediaTypes ? mediaTypes[BANNER] : {}); + if (banner) { + impObj.banner = banner; + } + } + if (mediaTypes && mediaTypes[VIDEO]) { + const video = createVideoRequest(bid, mediaTypes[VIDEO]); + if (video) { + impObj.video = video; + } + } + + if (impObj.banner || impObj.video) { + imp.push(impObj); + } + }); + + const source = { + tid: auctionId, + ext: { + wrapper: 'Prebid_js', + wrapper_version: '$prebid.version$' + } + }; + + if (schain) { + source.ext.schain = schain; + } + + const bidderTimeout = config.getConfig('bidderTimeout') || timeout; + const tmax = timeout ? Math.min(bidderTimeout, timeout) : bidderTimeout; + + let request = { + id: bidderRequestId, + site: { + page: referer + }, + tmax, + source, + imp + }; + + if (content) { + request.site.content = content; + } + + if (jwpseg && jwpseg.length) { + user = { + data: [{ + name: 'iow_labs_pub_data', + segment: jwpseg.map((seg) => { + return {name: 'jwpseg', value: seg}; + }) + }] + }; + } + + if (gdprConsent && gdprConsent.consentString) { + userExt = {consent: gdprConsent.consentString}; + } + + if (userId) { + if (userId.tdid) { + userExt = userExt || {}; + userExt.eids = userExt.eids || []; + userExt.eids.push({ + source: 'adserver.org', // Unified ID + uids: [{ + id: userId.tdid, + ext: { + rtiPartner: 'TDID' + } + }] + }); + } + if (userId.id5id && userId.id5id.uid) { + userExt = userExt || {}; + userExt.eids = userExt.eids || []; + userExt.eids.push({ + source: 'id5-sync.com', + uids: [{ + id: userId.id5id.uid + }], + ext: userId.id5id.ext + }); + } + if (userId.lipb && userId.lipb.lipbid) { + userExt = userExt || {}; + userExt.eids = userExt.eids || []; + userExt.eids.push({ + source: 'liveintent.com', + uids: [{ + id: userId.lipb.lipbid + }] + }); + } + if (userId.idl_env) { + userExt = userExt || {}; + userExt.eids = userExt.eids || []; + userExt.eids.push({ + source: 'identityLink', + uids: [{ + id: userId.idl_env + }] + }); + } + if (userId.criteoId) { + userExt = userExt || {}; + userExt.eids = userExt.eids || []; + userExt.eids.push({ + source: 'criteo.com', + uids: [{ + id: userId.criteoId + }] + }); + } + + if (userId.digitrustid && userId.digitrustid.data && userId.digitrustid.data.id) { + userExt = userExt || {}; + userExt.digitrust = Object.assign({}, userId.digitrustid.data); + } + } + + if (userExt && Object.keys(userExt).length) { + user = user || {}; + user.ext = userExt; + } + + if (user) { + request.user = user; + } + + const configKeywords = utils.transformBidderParamKeywords({ + 'user': utils.deepAccess(config.getConfig('fpd.user'), 'keywords') || null, + 'context': utils.deepAccess(config.getConfig('fpd.context'), 'keywords') || null }); - const requests = []; - if (newFormatBids.length) { - requests.push(buildNewRequest(newFormatBids, bidderRequest)); + + if (configKeywords.length) { + pageKeywords = (pageKeywords || []).concat(configKeywords); } - if (oldFormatBids.length) { - requests.push(buildOldRequest(oldFormatBids, bidderRequest)); + + if (pageKeywords && pageKeywords.length > 0) { + pageKeywords.forEach(deleteValues); + } + + if (pageKeywords) { + request.ext = { + keywords: pageKeywords + }; + } + + if (gdprConsent && gdprConsent.gdprApplies) { + request.regs = { + ext: { + gdpr: gdprConsent.gdprApplies ? 1 : 0 + } + } + } + + if (uspConsent) { + if (!request.regs) { + request.regs = {ext: {}}; + } + request.regs.ext.us_privacy = uspConsent; } - return requests; + + return { + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(request), + newFormat: true, + bidsMap + }; }, /** * Unpack the response from the server into a list of bids. @@ -108,6 +324,33 @@ export const spec = { } }; +/** + * Gets bidfloor + * @param {Object} mediaTypes + * @param {Object} bid + * @returns {Number} floor + */ +function _getFloor (mediaTypes, bid) { + const curMediaType = mediaTypes.video ? 'video' : 'banner'; + let floor = bid.params.bidFloor || 0; + + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: curMediaType, + size: bid.sizes.map(([w, h]) => ({w, h})) + }); + + if (typeof floorInfo === 'object' && + floorInfo.currency === 'USD' && + !isNaN(parseFloat(floorInfo.floor))) { + floor = Math.max(floor, parseFloat(floorInfo.floor)); + } + } + + return floor; +} + function isPopulatedArray(arr) { return !!(utils.isArray(arr) && arr.length > 0); } @@ -135,24 +378,7 @@ function _addBidResponse(serverBid, bidRequest, bidResponses) { if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid); if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); else { - let bid = null; - let slot = null; - const bidsMap = bidRequest.bidsMap; - if (bidRequest.newFormat) { - bid = bidsMap[serverBid.impid]; - } else { - const awaitingBids = bidsMap[serverBid.auid]; - if (awaitingBids) { - const sizeId = `${serverBid.w}x${serverBid.h}`; - if (awaitingBids[sizeId]) { - slot = awaitingBids[sizeId][0]; - bid = slot.bids.shift(); - } - } else { - errorMessage = LOG_ERROR_MESS.noPlacementCode + serverBid.auid; - } - } - + const bid = bidRequest.bidsMap[serverBid.impid]; if (bid) { const bidResponse = { requestId: bid.bidId, // bid.bidderRequestId, @@ -184,21 +410,6 @@ function _addBidResponse(serverBid, bidRequest, bidResponses) { bidResponse.mediaType = BANNER; } bidResponses.push(bidResponse); - - if (slot && !slot.bids.length) { - slot.parents.forEach(({parent, key, uid}) => { - const index = parent[key].indexOf(slot); - if (index > -1) { - parent[key].splice(index, 1); - } - if (!parent[key].length) { - delete parent[key]; - if (!utils.getKeys(parent).length) { - delete bidsMap[uid]; - } - } - }); - } } } if (errorMessage) { @@ -206,294 +417,6 @@ function _addBidResponse(serverBid, bidRequest, bidResponses) { } } -function buildOldRequest(validBidRequests, bidderRequest) { - const auids = []; - const bidsMap = {}; - const slotsMapByUid = {}; - const sizeMap = {}; - const bids = validBidRequests || []; - let pageKeywords = null; - let reqId; - - bids.forEach(bid => { - reqId = bid.bidderRequestId; - const {params: {uid}, adUnitCode, mediaTypes} = bid; - auids.push(uid); - const sizesId = utils.parseSizesInput(bid.sizes); - - if (!pageKeywords && !utils.isEmpty(bid.params.keywords)) { - pageKeywords = utils.transformBidderParamKeywords(bid.params.keywords); - } - - const addedSizes = {}; - sizesId.forEach((sizeId) => { - addedSizes[sizeId] = true; - }); - const bannerSizesId = utils.parseSizesInput(utils.deepAccess(mediaTypes, 'banner.sizes')); - const videoSizesId = utils.parseSizesInput(utils.deepAccess(mediaTypes, 'video.playerSize')); - bannerSizesId.concat(videoSizesId).forEach((sizeId) => { - if (!addedSizes[sizeId]) { - addedSizes[sizeId] = true; - sizesId.push(sizeId); - } - }); - - if (!slotsMapByUid[uid]) { - slotsMapByUid[uid] = {}; - } - const slotsMap = slotsMapByUid[uid]; - if (!slotsMap[adUnitCode]) { - slotsMap[adUnitCode] = {adUnitCode, bids: [bid], parents: []}; - } else { - slotsMap[adUnitCode].bids.push(bid); - } - const slot = slotsMap[adUnitCode]; - - sizesId.forEach((sizeId) => { - sizeMap[sizeId] = true; - if (!bidsMap[uid]) { - bidsMap[uid] = {}; - } - - if (!bidsMap[uid][sizeId]) { - bidsMap[uid][sizeId] = [slot]; - } else { - bidsMap[uid][sizeId].push(slot); - } - slot.parents.push({parent: bidsMap[uid], key: sizeId, uid}); - }); - }); - - const configKeywords = utils.transformBidderParamKeywords({ - 'user': utils.deepAccess(config.getConfig('fpd.user'), 'keywords') || null, - 'context': utils.deepAccess(config.getConfig('fpd.context'), 'keywords') || null - }); - - if (configKeywords.length) { - pageKeywords = (pageKeywords || []).concat(configKeywords); - } - - if (pageKeywords && pageKeywords.length > 0) { - pageKeywords.forEach(deleteValues); - } - - const payload = { - auids: auids.join(','), - sizes: utils.getKeys(sizeMap).join(','), - r: reqId, - wrapperType: 'Prebid_js', - wrapperVersion: '$prebid.version$' - }; - - if (pageKeywords) { - payload.keywords = JSON.stringify(pageKeywords); - } - - if (bidderRequest) { - if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { - payload.u = bidderRequest.refererInfo.referer; - } - if (bidderRequest.timeout) { - payload.wtimeout = bidderRequest.timeout; - } - if (bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.consentString) { - payload.gdpr_consent = bidderRequest.gdprConsent.consentString; - } - payload.gdpr_applies = - (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') - ? Number(bidderRequest.gdprConsent.gdprApplies) : 1; - } - if (bidderRequest.uspConsent) { - payload.us_privacy = bidderRequest.uspConsent; - } - } - - return { - method: 'GET', - url: ENDPOINT_URL, - data: utils.parseQueryStringParameters(payload).replace(/\&$/, ''), - bidsMap: bidsMap - } -} - -function buildNewRequest(validBidRequests, bidderRequest) { - let pageKeywords = null; - let jwpseg = null; - let content = null; - let schain = null; - let userId = null; - let user = null; - let userExt = null; - let {bidderRequestId, auctionId, gdprConsent, uspConsent, timeout, refererInfo} = bidderRequest; - - const referer = refererInfo ? encodeURIComponent(refererInfo.referer) : ''; - const imp = []; - const bidsMap = {}; - - validBidRequests.forEach((bid) => { - if (!bidderRequestId) { - bidderRequestId = bid.bidderRequestId; - } - if (!auctionId) { - auctionId = bid.auctionId; - } - if (!schain) { - schain = bid.schain; - } - if (!userId) { - userId = bid.userId; - } - const {params: {uid, keywords}, mediaTypes, bidId, adUnitCode, realTimeData} = bid; - bidsMap[bidId] = bid; - if (!pageKeywords && !utils.isEmpty(keywords)) { - pageKeywords = utils.transformBidderParamKeywords(keywords); - } - if (realTimeData && realTimeData.jwTargeting) { - if (!jwpseg && realTimeData.jwTargeting.segments) { - jwpseg = realTimeData.jwTargeting.segments; - } - if (!content && realTimeData.jwTargeting.content) { - content = realTimeData.jwTargeting.content; - } - } - let impObj = { - id: bidId, - tagid: uid.toString(), - ext: { - divid: adUnitCode - } - }; - - if (!mediaTypes || mediaTypes[BANNER]) { - const banner = createBannerRequest(bid, mediaTypes ? mediaTypes[BANNER] : {}); - if (banner) { - impObj.banner = banner; - } - } - if (mediaTypes && mediaTypes[VIDEO]) { - const video = createVideoRequest(bid, mediaTypes[VIDEO]); - if (video) { - impObj.video = video; - } - } - - if (impObj.banner || impObj.video) { - imp.push(impObj); - } - }); - - const source = { - tid: auctionId, - ext: { - wrapper: 'Prebid_js', - wrapper_version: '$prebid.version$' - } - }; - - if (schain) { - source.ext.schain = schain; - } - - const tmax = config.getConfig('bidderTimeout') || timeout; - - let request = { - id: bidderRequestId, - site: { - page: referer - }, - tmax, - source, - imp - }; - - if (content) { - request.site.content = content; - } - - if (jwpseg && jwpseg.length) { - user = { - data: [{ - name: 'iow_labs_pub_data', - segment: jwpseg.map((seg) => { - return {name: 'jwpseg', value: seg}; - }) - }] - }; - } - - if (gdprConsent && gdprConsent.consentString) { - userExt = {consent: gdprConsent.consentString}; - } - - if (userId) { - userExt = userExt || {}; - if (userId.tdid) { - userExt.unifiedid = userId.tdid; - } - if (userId.id5id && userId.id5id.uid) { - userExt.id5id = userId.id5id.uid; - } - if (userId.digitrustid && userId.digitrustid.data && userId.digitrustid.data.id) { - userExt.digitrustid = userId.digitrustid.data.id; - } - if (userId.lipb && userId.lipb.lipbid) { - userExt.liveintentid = userId.lipb.lipbid; - } - } - - if (userExt && Object.keys(userExt).length) { - user = user || {}; - user.ext = userExt; - } - - if (user) { - request.user = user; - } - - const configKeywords = utils.transformBidderParamKeywords({ - 'user': utils.deepAccess(config.getConfig('fpd.user'), 'keywords') || null, - 'context': utils.deepAccess(config.getConfig('fpd.context'), 'keywords') || null - }); - - if (configKeywords.length) { - pageKeywords = (pageKeywords || []).concat(configKeywords); - } - - if (pageKeywords && pageKeywords.length > 0) { - pageKeywords.forEach(deleteValues); - } - - if (pageKeywords) { - request.ext = { - keywords: pageKeywords - }; - } - - if (gdprConsent && gdprConsent.gdprApplies) { - request.regs = { - ext: { - gdpr: gdprConsent.gdprApplies ? 1 : 0 - } - } - } - - if (uspConsent) { - if (!request.regs) { - request.regs = {ext: {}}; - } - request.regs.ext.us_privacy = uspConsent; - } - - return { - method: 'POST', - url: NEW_ENDPOINT_URL, - data: JSON.stringify(request), - newFormat: true, - bidsMap - }; -} - function createVideoRequest(bid, mediaType) { const {playerSize, mimes, durationRangeSec} = mediaType; const size = (playerSize || bid.sizes || [])[0]; diff --git a/modules/gridBidAdapter.md b/modules/gridBidAdapter.md index 77b9bbf0f36..6a7075ccb00 100644 --- a/modules/gridBidAdapter.md +++ b/modules/gridBidAdapter.md @@ -20,7 +20,7 @@ Grid bid adapter supports Banner and Video (instream and outstream). bidder: "grid", params: { uid: '1', - priceType: 'gross' // by default is 'net' + bidFloor: 0.5 } } ] @@ -32,7 +32,6 @@ Grid bid adapter supports Banner and Video (instream and outstream). bidder: "grid", params: { uid: 2, - priceType: 'gross', keywords: { brandsafety: ['disaster'], topic: ['stress', 'fear'] diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 3206b7e1727..2cb5ce61064 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -403,6 +403,7 @@ function interpretResponse (serverResponse, bidRequest) { } = Object.assign(defaultResponse, serverResponseBody) let data = bidRequest.data || {} let product = data.pi + let mediaType = (product === 6 || product === 7) ? VIDEO : BANNER let isTestUnit = (product === 3 && data.si === 9) let sizes = utils.parseSizesInput(bidRequest.sizes) let [width, height] = sizes[0].split('x') @@ -424,9 +425,9 @@ function interpretResponse (serverResponse, bidRequest) { bidResponses.push({ // dealId: DEAL_ID, // referrer: REFERER, - ...(product === 7 && { vastXml: markup, mediaType: VIDEO }), ad: wrapper ? getWrapperCode(wrapper, Object.assign({}, serverResponseBody, { bidRequest })) : markup, - ...(product === 6 && {ad: markup}), + ...(mediaType === VIDEO && {ad: markup, vastXml: markup}), + mediaType, cpm: isTestUnit ? 0.1 : cpm, creativeId, currency: cur || 'USD', diff --git a/modules/gumgumBidAdapter.md b/modules/gumgumBidAdapter.md index f47666e9628..7b4f0c98ea7 100644 --- a/modules/gumgumBidAdapter.md +++ b/modules/gumgumBidAdapter.md @@ -8,7 +8,10 @@ Maintainer: engineering@gumgum.com # Description -GumGum adapter for Prebid.js 1.0 +GumGum adapter for Prebid.js +Please note that both video and in-video products require a mediaType of video. +All other products (in-screen, slot, native) should have a mediaType of banner. + # Test Parameters ``` @@ -16,6 +19,11 @@ var adUnits = [ { code: 'test-div', sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, bids: [ { bidder: 'gumgum', @@ -28,6 +36,11 @@ var adUnits = [ },{ code: 'test-div', sizes: [[300, 50]], + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + }, bids: [ { bidder: 'gumgum', @@ -40,6 +53,18 @@ var adUnits = [ },{ code: 'test-div', sizes: [[300, 50]], + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + minduration: 1, + maxduration: 2, + linearity: 2, + startdelay: 1, + placement: 1, + protocols: [1, 2] + } + } bids: [ { bidder: 'gumgum', diff --git a/modules/haloRtdProvider.js b/modules/haloRtdProvider.js new file mode 100644 index 00000000000..1ce44ca6004 --- /dev/null +++ b/modules/haloRtdProvider.js @@ -0,0 +1,197 @@ +/** + * This module adds audigent provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will fetch segments from audigent server + * @module modules/audigentRtdProvider + * @requires module:modules/realTimeData + */ +import {getGlobal} from '../src/prebidGlobal.js'; +import * as utils from '../src/utils.js'; +import {submodule} from '../src/hook.js'; +import {ajax} from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); + +/** @type {string} */ +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'halo'; + +export const HALOID_LOCAL_NAME = 'auHaloId'; +export const SEG_LOCAL_NAME = '__adgntseg'; + +const set = (obj, path, val) => { + const keys = path.split('.'); + const lastKey = keys.pop(); + const lastObj = keys.reduce((obj, key) => obj[key] = obj[key] || {}, obj); + lastObj[lastKey] = lastObj[lastKey] || val; +}; + +/** bid adapter format segment augmentation functions */ +const segmentMappers = { + appnexus: function(bid, segments) { + set(bid, 'params.user.segments', []); + let appnexusSegments = []; + segments.forEach(segment => { + if (typeof segment.id != 'undefined' && segment.id != null) { + appnexusSegments.push(parseInt(segment.id)); + } + }) + bid.params.user.segments = bid.params.user.segments.concat(appnexusSegments); + }, + generic: function(bid, segments) { + bid.segments = bid.segments || []; + if (Array.isArray(bid.segments)) { + bid.segments = bid.segments.concat(segments); + } + } +} + +/** + * decorate adUnits with segment data + * @param {adUnit[]} adUnits + * @param {Object} data + */ +export function addSegmentData(adUnits, segmentData, config) { + adUnits.forEach(adUnit => { + if (adUnit.hasOwnProperty('bids')) { + adUnit.bids.forEach(bid => { + try { + set(bid, 'fpd.user.data', []); + if (Array.isArray(bid.fpd.user.data)) { + bid.fpd.user.data.forEach(fpdData => { + let segments = segmentData[fpdData.id] || segmentData[fpdData.name] || []; + fpdData.segment = (fpdData.segment || []).concat(segments); + }); + } + } catch (err) { + utils.logError(err.message); + } + + try { + if (config.params.mapSegments && config.params.mapSegments[bid.bidder] && segmentData[bid.bidder]) { + if (typeof config.params.mapSegments[bid.bidder] == 'function') { + config.params.mapSegments[bid.bidder](bid, segmentData[bid.bidder]); + } else if (segmentMappers[bid.bidder]) { + segmentMappers[bid.bidder](bid, segmentData[bid.bidder]); + } + } + } catch (err) { + utils.logError(err.message); + } + }); + } + }); + + return adUnits; +} + +/** + * segment retrieval from audigent's backends + * @param {Object} reqBidsConfigObj + * @param {function} onDone + * @param {Object} config + * @param {Object} userConsent + */ +export function getSegments(reqBidsConfigObj, onDone, config, userConsent) { + const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + + if (config.params.segmentCache) { + let jsonData = storage.getDataFromLocalStorage(SEG_LOCAL_NAME); + + if (jsonData) { + let data = JSON.parse(jsonData); + + if (data.audigent_segments) { + addSegmentData(adUnits, data.audigent_segments, config); + onDone(); + return; + } + } + } + + const userIds = (getGlobal()).getUserIds(); + + let haloId = storage.getDataFromLocalStorage(HALOID_LOCAL_NAME); + if (haloId) { + userIds.haloId = haloId; + getSegmentsAsync(adUnits, onDone, config, userConsent, userIds); + } else { + var script = document.createElement('script') + script.type = 'text/javascript'; + + script.onload = function() { + userIds.haloId = storage.getDataFromLocalStorage(HALOID_LOCAL_NAME); + getSegmentsAsync(adUnits, onDone, config, userConsent, userIds); + } + + script.src = 'https://id.halo.ad.gt/api/v1/haloid'; + document.getElementsByTagName('head')[0].appendChild(script); + } +} + +/** + * async segment retrieval from audigent's backends + * @param {adUnit[]} adUnits + * @param {function} onDone + * @param {Object} config + * @param {Object} userConsent + * @param {Object} userIds + */ +export function getSegmentsAsync(adUnits, onDone, config, userConsent, userIds) { + let reqParams = {}; + if (typeof config == 'object' && config != null) { + set(config, 'params.requestParams', {}); + reqParams = config.params.requestParams; + } + + const url = `https://seg.halo.ad.gt/api/v1/rtb_segments`; + ajax(url, { + success: function (response, req) { + if (req.status === 200) { + try { + const data = JSON.parse(response); + if (data && data.audigent_segments) { + addSegmentData(adUnits, data.audigent_segments, config); + onDone(); + storage.setDataInLocalStorage(SEG_LOCAL_NAME, JSON.stringify(data)); + } else { + onDone(); + } + } catch (err) { + utils.logError('unable to parse audigent segment data'); + onDone(); + } + } else if (req.status === 204) { + // unrecognized partner config + onDone(); + } + }, + error: function () { + onDone(); + utils.logError('unable to get audigent segment data'); + } + }, + JSON.stringify({'userIds': userIds, 'config': reqParams}), + {contentType: 'application/json'} + ); +} + +/** + * module init + * @param {Object} provider + * @param {Objkect} userConsent + * @return {boolean} + */ +function init(provider, userConsent) { + return true; +} + +/** @type {RtdSubmodule} */ +export const haloSubmodule = { + name: SUBMODULE_NAME, + getBidRequestData: getSegments, + init: init +}; + +submodule(MODULE_NAME, haloSubmodule); diff --git a/modules/haloRtdProvider.md b/modules/haloRtdProvider.md new file mode 100644 index 00000000000..2897a5917fa --- /dev/null +++ b/modules/haloRtdProvider.md @@ -0,0 +1,132 @@ +## Audigent Halo Real-time Data Submodule + +Audigent is a next-generation data management platform and a first-of-a-kind +"data agency" containing some of the most exclusive content-consuming audiences +across desktop, mobile and social platforms. + +This real-time data module provides quality user segmentation that can be +attached to bid request objects destined for different SSPs in order to optimize +targeting. Audigent maintains a large database of first-party Tradedesk Unified +ID, Audigent Halo ID and other id provider mappings to various third-party +segment types that are utilizable across different SSPs. With this module, +these segments can be retrieved and supplied to the SSP in real-time during +the bid request cycle. + +### Publisher Usage + +Compile the Halo RTD module into your Prebid build: + +`gulp build --modules=userId,unifiedIdSystem,rtdModule,audigentRtdProvider,appnexusBidAdapter` + +Add the Halo RTD provider to your Prebid config. For any adapters +that you would like to retrieve segments for, add a mapping in the 'mapSegments' +parameter. In this example we will configure publisher 1234 to retrieve +appnexus segments from Audigent. See the "Parameter Descriptions" below for +more detailed information of the configuration parameters. Currently, +OpenRTB compatible fpd data will be added for any bid adapter in the +"mapSegments" objects. Automated bid augmentation exists for some bidders. +Please work with your Audigent Prebid support team (prebid@audigent.com) on +which version of Prebid.js supports which bidders automatically. + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: auctionDelay, + dataProviders: [ + { + name: "halo", + waitForIt: true, + params: { + mapSegments: { + appnexus: true, + }, + segmentCache: false, + requestParams: { + publisherId: 1234 + } + } + } + ] + } + ... +} +``` + +### Parameter Descriptions for the Halo `dataProviders` Configuration Section + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| name | String | Real time data module name | Always 'halo' | +| waitForIt | Boolean | Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false | +| params | Object | | | +| params.mapSegments | Boolean | Dictionary of bidders you would like to supply Audigent segments for. Maps to boolean values, but also allows functions for custom mapping logic. The function signature is (bid, segments) => {}. | Required | +| params.segmentCache | Boolean | This parameter tells the Halo RTD module to attempt reading segments from a local storage cache instead of always requesting them from the Audigent server. | Optional. Defaults to false. | +| params.requestParams | Object | Publisher partner specific configuration options, such as optional publisher id and other segment query related metadata to be submitted to Audigent's backend with each request. Contact prebid@audigent.com for more information. | Optional | + +### Overriding & Adding Segment Mappers +As indicated above, it is possible to provide your own bid augmentation +functions. This is useful if you know a bid adapter's API supports segment +fields which aren't specifically being added to request objects in the Prebid +bid adapter. You can also override segment mappers by passing a function +instead of a boolean to the Halo RTD segment module. This might be useful +if you'd like to use custom logic to determine which segments are sent +to a specific backend. + +Please see the following example, which provides a function to modify bids for +a bid adapter called adBuzz and overrides the appnexus segment mapper. + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: auctionDelay, + dataProviders: [ + { + name: "halo", + waitForIt: true, + params: { + mapSegments: { + // adding an adBuzz segment mapper + adBuzz: function(bid, segments) { + bid.params.adBuzzCustomSegments = []; + for (var i = 0; i < segments.length; i++) { + bid.params.adBuzzCustomSegments.push(segments[i].id); + } + }, + // overriding the appnexus segment mapper to exclude certain segments + appnexus: function(bid, segments) { + for (var i = 0; i < segments.length; i++) { + if (segments[i].id != 'exclude_segment') { + bid.params.user.segments.push(segments[i].id); + } + } + } + }, + segmentCache: false, + requestParams: { + publisherId: 1234 + } + } + } + ] + } + ... +} +``` + +More examples can be viewed in the haloRtdAdapter_spec.js tests. + +### Testing + +To view an example of available segments returned by Audigent's backends: + +`gulp serve --modules=userId,unifiedIdSystem,rtdModule,haloRtdProvider,appnexusBidAdapter` + +and then point your browser at: + +`http://localhost:9999/integrationExamples/gpt/haloRtdProvider_example.html` + + + + diff --git a/modules/haxmediaBidAdapter.js b/modules/haxmediaBidAdapter.js new file mode 100644 index 00000000000..c4ce2eb3663 --- /dev/null +++ b/modules/haxmediaBidAdapter.js @@ -0,0 +1,107 @@ +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 = 'haxmedia'; +const AD_URL = 'https://balancer.haxmedia.io/?c=o&m=multi'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency) { + return false; + } + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers); + default: + return false; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.placementId))); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + let winTop = window; + let location; + try { + location = new URL(bidderRequest.refererInfo.referer) + winTop = window.top; + } catch (e) { + location = winTop.location; + utils.logMessage(e); + }; + + const placements = []; + const request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + 'secure': 1, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent + } + } + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const placement = { + placementId: bid.params.placementId, + bidId: bid.bidId, + schain: bid.schain || {}, + }; + const mediaType = bid.mediaTypes + + if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { + placement.sizes = mediaType[BANNER].sizes; + placement.traffic = BANNER; + } else if (mediaType && mediaType[VIDEO] && mediaType[VIDEO].playerSize) { + placement.wPlayer = mediaType[VIDEO].playerSize[0]; + placement.hPlayer = mediaType[VIDEO].playerSize[1]; + placement.traffic = VIDEO; + } else if (mediaType && mediaType[NATIVE]) { + placement.native = mediaType[NATIVE]; + placement.traffic = NATIVE; + } + placements.push(placement); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + response.push(resItem); + } + } + return response; + }, +}; + +registerBidder(spec); diff --git a/modules/haxmediaBidAdapter.md b/modules/haxmediaBidAdapter.md new file mode 100644 index 00000000000..f661a9e4e71 --- /dev/null +++ b/modules/haxmediaBidAdapter.md @@ -0,0 +1,72 @@ +# Overview + +``` +Module Name: haxmedia Bidder Adapter +Module Type: haxmedia Bidder Adapter +Maintainer: haxmixqk@haxmediapartners.io +``` + +# Description + +Module that connects to haxmedia demand sources + +# Test Parameters +``` + var adUnits = [ + { + code:'1', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'haxmedia', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids:[ + { + bidder: 'haxmedia', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + native: { + title: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids:[ + { + bidder: 'haxmedia', + params: { + placementId: 0 + } + } + ] + } + ]; +``` diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index f8ff50f52a3..7033a71d015 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -10,11 +10,17 @@ import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { getRefererInfo } from '../src/refererDetection.js'; import { getStorageManager } from '../src/storageManager.js'; +import { uspDataHandler } from '../src/adapterManager.js'; const MODULE_NAME = 'id5Id'; const GVLID = 131; -const BASE_NB_COOKIE_NAME = 'id5id.1st'; -const NB_COOKIE_EXP_DAYS = (30 * 24 * 60 * 60 * 1000); // 30 days +const NB_EXP_DAYS = 30; +export const ID5_STORAGE_NAME = 'id5id'; +const LOCAL_STORAGE = 'html5'; + +// order the legacy cookie names in reverse priority order so the last +// cookie in the array is the most preferred to use +const LEGACY_COOKIE_NAMES = [ 'pbjs-id5id', 'id5id.1st' ]; const storage = getStorageManager(GVLID, MODULE_NAME); @@ -42,10 +48,7 @@ export const id5IdSubmodule = { let uid; let linkType = 0; - if (value && typeof value.ID5ID === 'string') { - // don't lose our legacy value from cache - uid = value.ID5ID; - } else if (value && typeof value.universal_uid === 'string') { + if (value && typeof value.universal_uid === 'string') { uid = value.universal_uid; linkType = value.link_type || linkType; } else { @@ -71,26 +74,27 @@ export const id5IdSubmodule = { * @returns {IdResponse|undefined} */ getId(config, consentData, cacheIdObj) { - const configParams = (config && config.params) || {}; - if (!hasRequiredParams(configParams)) { + if (!hasRequiredConfig(config)) { return undefined; } + + const url = `https://id5-sync.com/g/v2/${config.params.partner}.json`; const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; - const gdprConsentString = hasGdpr ? consentData.consentString : ''; - const url = `https://id5-sync.com/g/v2/${configParams.partner}.json?gdpr_consent=${gdprConsentString}&gdpr=${hasGdpr}`; const referer = getRefererInfo(); - const signature = (cacheIdObj && cacheIdObj.signature) ? cacheIdObj.signature : ''; - const pubId = (cacheIdObj && cacheIdObj.ID5ID) ? cacheIdObj.ID5ID : ''; // TODO: remove when 1puid isn't needed + const signature = (cacheIdObj && cacheIdObj.signature) ? cacheIdObj.signature : getLegacyCookieSignature(); const data = { - 'partner': configParams.partner, - '1puid': pubId, // TODO: remove when 1puid isn't needed - 'nbPage': incrementNb(configParams), + 'gdpr': hasGdpr, + 'gdpr_consent': hasGdpr ? consentData.consentString : '', + 'partner': config.params.partner, + 'nbPage': incrementNb(config.params.partner), 'o': 'pbjs', - 'pd': configParams.pd || '', + 'pd': config.params.pd || '', + 'provider': config.params.provider || '', 'rf': referer.referer, 's': signature, 'top': referer.reachedTop ? 1 : 0, 'u': referer.stack[0] || window.location.href, + 'us_privacy': uspDataHandler.getConsentData() || '', 'v': '$prebid.version$' }; @@ -101,7 +105,13 @@ export const id5IdSubmodule = { if (response) { try { responseObj = JSON.parse(response); - resetNb(configParams); + resetNb(config.params.partner); + + // TODO: remove after requiring publishers to use localstorage and + // all publishers have upgraded + if (config.storage.type === LOCAL_STORAGE) { + removeLegacyCookies(config.params.partner); + } } catch (error) { utils.logError(error); } @@ -109,7 +119,7 @@ export const id5IdSubmodule = { callback(responseObj); }, error: error => { - utils.logError(`id5Id: ID fetch encountered an error`, error); + utils.logError(`User ID - ID5 submodule getId fetch encountered an error`, error); callback(); } }; @@ -129,39 +139,112 @@ export const id5IdSubmodule = { * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ extendId(config, cacheIdObj) { - const configParams = (config && config.params) || {}; - incrementNb(configParams); + const partnerId = (config && config.params && config.params.partner) || 0; + incrementNb(partnerId); return cacheIdObj; } }; -function hasRequiredParams(configParams) { - if (!configParams || typeof configParams.partner !== 'number') { +function hasRequiredConfig(config) { + if (!config || !config.params || !config.params.partner || typeof config.params.partner !== 'number') { utils.logError(`User ID - ID5 submodule requires partner to be defined as a number`); return false; } + + if (!config.storage || !config.storage.type || !config.storage.name) { + utils.logError(`User ID - ID5 submodule requires storage to be set`); + return false; + } + + // TODO: in a future release, return false if storage type or name are not set as required + if (config.storage.type !== LOCAL_STORAGE) { + utils.logWarn(`User ID - ID5 submodule recommends storage type to be '${LOCAL_STORAGE}'. In a future release this will become a strict requirement`); + } + // TODO: in a future release, return false if storage type or name are not set as required + if (config.storage.name !== ID5_STORAGE_NAME) { + utils.logWarn(`User ID - ID5 submodule recommends storage name to be '${ID5_STORAGE_NAME}'. In a future release this will become a strict requirement`); + } + return true; } -function nbCookieName(configParams) { - return hasRequiredParams(configParams) ? `${BASE_NB_COOKIE_NAME}_${configParams.partner}_nb` : undefined; + +export function expDaysStr(expDays) { + return (new Date(Date.now() + (1000 * 60 * 60 * 24 * expDays))).toUTCString(); } -function nbCookieExpStr(expDays) { - return (new Date(Date.now() + expDays)).toUTCString(); + +export function nbCacheName(partnerId) { + return `${ID5_STORAGE_NAME}_${partnerId}_nb`; } -function storeNbInCookie(configParams, nb) { - storage.setCookie(nbCookieName(configParams), nb, nbCookieExpStr(NB_COOKIE_EXP_DAYS), 'Lax'); +export function storeNbInCache(partnerId, nb) { + storeInLocalStorage(nbCacheName(partnerId), nb, NB_EXP_DAYS); } -function getNbFromCookie(configParams) { - const cacheNb = storage.getCookie(nbCookieName(configParams)); +export function getNbFromCache(partnerId) { + let cacheNb = getFromLocalStorage(nbCacheName(partnerId)); return (cacheNb) ? parseInt(cacheNb) : 0; } -function incrementNb(configParams) { - const nb = (getNbFromCookie(configParams) + 1); - storeNbInCookie(configParams, nb); +function incrementNb(partnerId) { + const nb = (getNbFromCache(partnerId) + 1); + storeNbInCache(partnerId, nb); return nb; } -function resetNb(configParams) { - storeNbInCookie(configParams, 0); +function resetNb(partnerId) { + storeNbInCache(partnerId, 0); +} + +function getLegacyCookieSignature() { + let legacyStoredValue; + LEGACY_COOKIE_NAMES.forEach(function(cookie) { + if (storage.getCookie(cookie)) { + legacyStoredValue = JSON.parse(storage.getCookie(cookie)) || legacyStoredValue; + } + }); + return (legacyStoredValue && legacyStoredValue.signature) || ''; +} + +/** + * Remove our legacy cookie values. Needed until we move all publishers + * to html5 storage in a future release + * @param {integer} partnerId + */ +function removeLegacyCookies(partnerId) { + LEGACY_COOKIE_NAMES.forEach(function(cookie) { + storage.setCookie(`${cookie}`, '', expDaysStr(-1)); + storage.setCookie(`${cookie}_nb`, '', expDaysStr(-1)); + storage.setCookie(`${cookie}_${partnerId}_nb`, '', expDaysStr(-1)); + storage.setCookie(`${cookie}_last`, '', expDaysStr(-1)); + }); +} + +/** + * This will make sure we check for expiration before accessing local storage + * @param {string} key + */ +export function getFromLocalStorage(key) { + const storedValueExp = storage.getDataFromLocalStorage(`${key}_exp`); + // empty string means no expiration set + if (storedValueExp === '') { + return storage.getDataFromLocalStorage(key); + } else if (storedValueExp) { + if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { + return storage.getDataFromLocalStorage(key); + } + } + // if we got here, then we have an expired item or we didn't set an + // expiration initially somehow, so we need to remove the item from the + // local storage + storage.removeDataFromLocalStorage(key); + return null; +} +/** + * Ensure that we always set an expiration in local storage since + * by default it's not required + * @param {string} key + * @param {any} value + * @param {integer} expDays + */ +export function storeInLocalStorage(key, value, expDays) { + storage.setDataInLocalStorage(`${key}_exp`, expDaysStr(expDays)); + storage.setDataInLocalStorage(`${key}`, value); } submodule('userId', id5IdSubmodule); diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md new file mode 100644 index 00000000000..e5e3969c19c --- /dev/null +++ b/modules/id5IdSystem.md @@ -0,0 +1,55 @@ +# ID5 Universal ID + +The ID5 Universal ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 Universal ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 Universal ID and detailed integration docs, please visit [our documentation](https://console.id5.io/docs/public/prebid). We also recommend that you sign up for our [release notes](https://id5.io/universal-id/release-notes) to stay up-to-date with any changes to the implementation of the ID5 Universal ID in Prebid. + +## ID5 Universal ID Registration + +The ID5 Universal ID is free to use, but requires a simple registration with ID5. Please visit [id5.io/universal-id](https://id5.io/universal-id) to sign up and request your ID5 Partner Number to get started. + +The ID5 privacy policy is at [https://www.id5.io/platform-privacy-policy](https://www.id5.io/platform-privacy-policy). + +## ID5 Universal ID Configuration + +First, make sure to add the ID5 submodule to your Prebid.js package with: + +``` +gulp build --modules=id5IdSystem,userId +``` + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: "id5Id", + params: { + partner: 173, // change to the Partner Number you received from ID5 + pd: "MT1iNTBjY..." // optional, see table below for a link to how to generate this + }, + storage: { + type: "html5", // "html5" is the required storage type + name: "id5id", // "id5id" is the required storage name + expires: 90, // storage lasts for 90 days + refreshInSeconds: 8*3600 // refresh ID every 8 hours to ensure it's fresh + } + }], + auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules + } +}); +``` + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | +| params | Required | Object | Details for the ID5 Universal ID. | | +| params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | +| params.pd | Optional | String | Publisher-supplied data used for linking ID5 IDs across domains. See [our documentation](https://wiki.id5.io/x/BIAZ) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | +| params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | +| storage | Required | Object | Storage settings for how the User ID module will cache the ID5 ID locally | | +| storage.type | Required | String | This is where the results of the user ID will be stored. ID5 **requires** `"html5"`. | `"html5"` | +| storage.name | Required | String | The name of the local storage where the user ID will be stored. ID5 **requires** `"id5id"`. | `"id5id"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. ID5 recommends `90`. | `90` | +| storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 8 hours between refreshes | `8*3600` | + +**ATTENTION:** As of Prebid.js v4.14.0, ID5 requires `storage.type` to be `"html5"` and `storage.name` to be `"id5id"`. Using other values will display a warning today, but in an upcoming release, it will prevent the ID5 module from loading. This change is to ensure the ID5 module in Prebid.js interoperates properly with the [ID5 API](https://github.com/id5io/id5-api.js) and to reduce the size of publishers' first-party cookies that are sent to their web servers. If you have any questions, please reach out to us at [prebid@id5.io](mailto:prebid@id5.io). diff --git a/modules/idLibrary.js b/modules/idLibrary.js new file mode 100644 index 00000000000..ba3cc0b5efb --- /dev/null +++ b/modules/idLibrary.js @@ -0,0 +1,243 @@ +import {getGlobal} from '../src/prebidGlobal.js'; +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import * as utils from '../src/utils.js'; +import MD5 from 'crypto-js/md5.js'; + +let email; +let conf; +const LOG_PRE_FIX = 'ID-Library: '; +const CONF_DEFAULT_OBSERVER_DEBOUNCE_MS = 250; +const CONF_DEFAULT_FULL_BODY_SCAN = true; +const OBSERVER_CONFIG = { + subtree: true, + attributes: true, + attributeOldValue: false, + childList: true, + attirbuteFilter: ['value'], + characterData: true, + characterDataOldValue: false +}; +const logInfo = createLogInfo(LOG_PRE_FIX); +const logError = createLogError(LOG_PRE_FIX); + +function createLogInfo(prefix) { + return function (...strings) { + utils.logInfo(prefix + ' ', ...strings); + } +} + +function createLogError(prefix) { + return function (...strings) { + utils.logError(prefix + ' ', ...strings); + } +} + +function getEmail(value) { + const matched = value.match(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/gi); + if (!matched) { + return null; + } + logInfo('Email found' + matched[0]); + return matched[0]; +} + +function bodyAction(mutations, observer) { + logInfo('BODY observer on debounce called'); + // If the email is found in the input element, disconnect the observer + if (email) { + observer.disconnect(); + logInfo('Email is found, body observer disconnected'); + return; + } + + const body = document.body.innerHTML; + email = getEmail(body); + if (email !== null) { + logInfo(`Email obtained from the body ${email}`); + observer.disconnect(); + logInfo('Post data on email found in body'); + postData(); + } +} + +function targetAction(mutations, observer) { + logInfo('Target observer called'); + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + email = node.textContent; + + if (email) { + logInfo('Email obtained from the target ' + email); + observer.disconnect(); + logInfo('Post data on email found in target'); + postData(); + return; + } + } + } +} + +function addInputElementsElementListner(conf) { + logInfo('Adding input element listeners'); + const inputs = document.querySelectorAll('input[type=text], input[type=email]'); + + for (var i = 0; i < inputs.length; i++) { + logInfo(`Original Value in Input = ${inputs[i].value}`); + inputs[i].addEventListener('change', event => processInputChange(event)); + inputs[i].addEventListener('blur', event => processInputChange(event)); + } +} + +function removeInputElementsElementListner() { + logInfo('Removing input element listeners'); + const inputs = document.querySelectorAll('input[type=text], input[type=email]'); + + for (var i = 0; i < inputs.length; i++) { + inputs[i].removeEventListener('change', event => processInputChange(event)); + inputs[i].removeEventListener('blur', event => processInputChange(event)); + } +} + +function processInputChange(event) { + const value = event.target.value; + logInfo(`Modified Value of input ${event.target.value}`); + email = getEmail(value); + if (email !== null) { + logInfo('Email found in input ' + email); + postData(); + removeInputElementsElementListner(); + } +} + +function debounce(func, wait, immediate) { + var timeout; + return function () { + const context = this; + const args = arguments; + const later = function () { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + if (callNow) { + func.apply(context, args); + } else { + logInfo('Debounce wait time ' + wait); + timeout = setTimeout(later, wait); + } + }; +}; + +function handleTargetElement() { + const targetObserver = new MutationObserver(debounce(targetAction, conf.debounce, false)); + + const targetElement = document.getElementById(conf.target); + if (targetElement) { + email = targetElement.innerText; + + if (!email) { + logInfo('Finding the email with observer'); + targetObserver.observe(targetElement, OBSERVER_CONFIG); + } else { + logInfo('Target found with target ' + email); + logInfo('Post data on email found in target with target'); + postData(); + } + } +} + +function handleBodyElements() { + if (doesInputElementsHaveEmail()) { + logInfo('Email found in input elements ' + email); + logInfo('Post data on email found in target without'); + postData(); + return; + } + email = getEmail(document.body.innerHTML); + if (email !== null) { + logInfo('Email found in body ' + email); + logInfo('Post data on email found in the body without observer'); + postData(); + return; + } + addInputElementsElementListner(); + if (conf.fullscan === true) { + const bodyObserver = new MutationObserver(debounce(bodyAction, conf.debounce, false)); + bodyObserver.observe(document.body, OBSERVER_CONFIG); + } +} + +function doesInputElementsHaveEmail() { + const inputs = document.getElementsByTagName('input'); + + for (let index = 0; index < inputs.length; ++index) { + const curInput = inputs[index]; + email = getEmail(curInput.value); + if (email !== null) { + return true; + } + } + return false; +} + +function syncCallback() { + return { + success: function () { + logInfo('Data synced successfully.'); + }, + error: function () { + logInfo('Data sync failed.'); + } + } +} + +function postData() { + (getGlobal()).refreshUserIds(); + const userIds = (getGlobal()).getUserIds(); + if (Object.keys(userIds).length === 0) { + logInfo('No user ids'); + return; + } + logInfo('Users' + JSON.stringify(userIds)); + const syncPayload = {}; + syncPayload.hid = MD5(email).toString(); + syncPayload.uids = JSON.stringify(userIds); + const payloadString = JSON.stringify(syncPayload); + logInfo(payloadString); + ajax(conf.url, syncCallback(), payloadString, {method: 'POST', withCredentials: true}); +} + +function associateIds() { + if (window.MutationObserver || window.WebKitMutationObserver) { + if (conf.target) { + handleTargetElement(); + } else { + handleBodyElements(); + } + } +} + +export function setConfig(config) { + if (!config) { + logError('Required confirguration not provided'); + return; + } + if (!config.url) { + logError('The required url is not configured'); + return; + } + if (typeof config.debounce !== 'number') { + config.debounce = CONF_DEFAULT_OBSERVER_DEBOUNCE_MS; + logInfo('Set default observer debounce to ' + CONF_DEFAULT_OBSERVER_DEBOUNCE_MS); + } + if (typeof config.fullscan !== 'boolean') { + config.fullscan = CONF_DEFAULT_FULL_BODY_SCAN; + logInfo('Set default fullscan ' + CONF_DEFAULT_FULL_BODY_SCAN); + } + conf = config; + associateIds(); +} + +config.getConfig('idLibrary', config => setConfig(config.idLibrary)); diff --git a/modules/idLibrary.md b/modules/idLibrary.md new file mode 100644 index 00000000000..69b63dc466b --- /dev/null +++ b/modules/idLibrary.md @@ -0,0 +1,24 @@ +## ID Library Configuration Example + + +|Param |Required |Description | +|----------------|-------------------------------|-----------------------------| +|url |Yes | The url endpoint is used to post the hashed email and user ids. | +|target |No |It should contain the element id from which the email can be read. | +|debounce |No | Time in milliseconds before the email and ids are fetched | +|fullscan |No | Option to enable/disable full body scan to get email. By default the full body scan is enabled. | + +### Example +``` + pbjs.setConfig({ + idLibrary:{ + url: , + debounce: 250, + target: 'username', + fullscan: false + }, + }); +``` + + +``` diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 3c000258ede..0e2d8f6f7dd 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -3,6 +3,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; +import { createEidsArray } from './userId/eids.js'; const BIDDER_CODE = 'improvedigital'; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; @@ -56,6 +57,13 @@ export const spec = { requestParameters.schain = bidRequests[0].schain; + if (bidRequests[0].userId) { + const eids = createEidsArray(bidRequests[0].userId); + if (eids.length) { + utils.deepSetValue(requestParameters, 'user.ext.eids', eids); + } + } + let requestObj = idClient.createRequest( normalizedBids, // requestObject requestParameters @@ -552,6 +560,9 @@ export function ImproveDigitalAdServerJSClient(endPoint) { if (requestParameters.schain) { impressionBidRequestObject.schain = requestParameters.schain; } + if (requestParameters.user) { + impressionBidRequestObject.user = requestParameters.user; + } if (extraRequestParameters) { for (let prop in extraRequestParameters) { impressionBidRequestObject[prop] = extraRequestParameters[prop]; diff --git a/modules/ironsourceBidAdapter.js b/modules/ironsourceBidAdapter.js index 922e7b94c5b..e23c0cb857d 100644 --- a/modules/ironsourceBidAdapter.js +++ b/modules/ironsourceBidAdapter.js @@ -48,7 +48,7 @@ export const spec = { creativeId: body.requestId, currency: body.currency, netRevenue: body.netRevenue, - ttl: TTL, + ttl: body.ttl || TTL, vastXml: body.vastXml, mediaType: VIDEO }; diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 77d4220e59a..cb37e4735c5 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -235,9 +235,9 @@ function addUserEids(userEids, seenIdPartners, id, source, ixlPartnerName, rtiPa * * @param {array} validBidRequests A list of valid bid request config objects. * @param {object} bidderRequest An object containing other info like gdprConsent. - * @param {array} impressions List of impression objects describing the bids. + * @param {object} impressions An object containing a list of impression objects describing the bids for each transactionId * @param {array} version Endpoint version denoting banner or video. - * @return {object} Info describing the request to the server. + * @return {array} List of objects describing the request to the server. * */ function buildRequest(validBidRequests, bidderRequest, impressions, version) { @@ -280,11 +280,10 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { // Since bidderRequestId are the same for different bid request, just use the first one. r.id = validBidRequests[0].bidderRequestId; - r.imp = impressions; - r.site = {}; r.ext = {}; r.ext.source = 'prebid'; + r.ext.ixdiag = {}; // if an schain is provided, send it along if (validBidRequests[0].schain) { @@ -358,30 +357,140 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { if (typeof otherIxConfig.timeout === 'number') { payload.t = otherIxConfig.timeout; } + + if (typeof otherIxConfig.detectMissingSizes === 'boolean') { + r.ext.ixdiag.dms = otherIxConfig.detectMissingSizes; + } else { + r.ext.ixdiag.dms = true; + } } // Use the siteId in the first bid request as the main siteId. payload.s = validBidRequests[0].params.siteId; payload.v = version; - payload.r = JSON.stringify(r); payload.ac = 'j'; payload.sd = 1; if (version === VIDEO_ENDPOINT_VERSION) { payload.nf = 1; } - return { + const requests = []; + + const request = { method: 'GET', url: baseUrl, data: payload }; + + const BASE_REQ_SIZE = new Blob([`${request.url}${utils.parseQueryStringParameters({...request.data, r: JSON.stringify(r)})}`]).size; + let currReqSize = BASE_REQ_SIZE; + + const MAX_REQ_SIZE = 8000; + const MAX_REQ_LIMIT = 4; + let sn = 0; + let msi = 0; + let msd = 0; + r.ext.ixdiag.msd = 0; + r.ext.ixdiag.msi = 0; + r.imp = []; + let i = 0; + const transactionIds = Object.keys(impressions); + let currMissingImps = []; + + while (i < transactionIds.length && requests.length < MAX_REQ_LIMIT) { + if (impressions[transactionIds[i]].hasOwnProperty('missingCount')) { + msd = impressions[transactionIds[i]].missingCount; + } + + trimImpressions(impressions[transactionIds[i]], MAX_REQ_SIZE - BASE_REQ_SIZE); + + if (impressions[transactionIds[i]].hasOwnProperty('missingImps')) { + msi = impressions[transactionIds[i]].missingImps.length; + } + + let currImpsSize = new Blob([encodeURIComponent(JSON.stringify(impressions[transactionIds[i]]))]).size; + currReqSize += currImpsSize; + if (currReqSize < MAX_REQ_SIZE) { + // pushing ix configured sizes first + r.imp.push(...impressions[transactionIds[i]].ixImps); + // update msd msi + r.ext.ixdiag.msd += msd; + r.ext.ixdiag.msi += msi; + + if (impressions[transactionIds[i]].hasOwnProperty('missingImps')) { + currMissingImps.push(...impressions[transactionIds[i]].missingImps); + } + + i++; + } else { + // pushing missing sizes after configured ones + const clonedPayload = utils.deepClone(payload); + + r.imp.push(...currMissingImps); + r.ext.ixdiag.sn = sn; + clonedPayload.sn = sn; + sn++; + clonedPayload.r = JSON.stringify(r); + + requests.push({ + method: 'GET', + url: baseUrl, + data: clonedPayload + }); + currMissingImps = []; + currReqSize = BASE_REQ_SIZE; + r.imp = []; + msd = 0; + msi = 0; + r.ext.ixdiag.msd = 0; + r.ext.ixdiag.msi = 0; + } + } + + if (currReqSize > BASE_REQ_SIZE && currReqSize < MAX_REQ_SIZE && requests.length < MAX_REQ_LIMIT) { + const clonedPayload = utils.deepClone(payload); + r.imp.push(...currMissingImps); + + if (requests.length > 0) { + r.ext.ixdiag.sn = sn; + clonedPayload.sn = sn; + } + clonedPayload.r = JSON.stringify(r); + + requests.push({ + method: 'GET', + url: baseUrl, + data: clonedPayload + }); + } + + return requests; } +/** + * + * @param {Object} impressions containing ixImps and possibly missingImps + * + */ +function trimImpressions(impressions, maxSize) { + let currSize = new Blob([encodeURIComponent(JSON.stringify(impressions))]).size; + if (currSize < maxSize) { + return; + } + while (currSize > maxSize) { + if (impressions.hasOwnProperty('missingImps') && impressions.missingImps.length > 0) { + impressions.missingImps.pop(); + } else if (impressions.hasOwnProperty('ixImps') && impressions.ixImps.length > 0) { + impressions.ixImps.pop(); + } + currSize = new Blob([encodeURIComponent(JSON.stringify(impressions))]).size; + } +} /** * * @param {array} bannerSizeList list of banner sizes * @param {array} bannerSize the size to be removed - * @return {boolean} true if succesfully removed, false if not found + * @return {boolean} true if successfully removed, false if not found */ function removeFromSizes(bannerSizeList, bannerSize) { @@ -496,19 +605,32 @@ export const spec = { */ buildRequests: function (validBidRequests, bidderRequest) { let reqs = []; - let bannerImps = []; - let videoImps = []; + let bannerImps = {}; + let videoImps = {}; let validBidRequest = null; // To capture the missing sizes i.e not configured for ix let missingBannerSizes = {}; + const DEFAULT_IX_CONFIG = { + detectMissingSizes: true, + }; + + const ixConfig = {...DEFAULT_IX_CONFIG, ...config.getConfig('ix')}; + for (let i = 0; i < validBidRequests.length; i++) { validBidRequest = validBidRequests[i]; if (validBidRequest.mediaType === VIDEO || utils.deepAccess(validBidRequest, 'mediaTypes.video')) { if (validBidRequest.mediaType === VIDEO || includesSize(validBidRequest.mediaTypes.video.playerSize, validBidRequest.params.size)) { - videoImps.push(bidToVideoImp(validBidRequest)); + if (!videoImps.hasOwnProperty(validBidRequest.transactionId)) { + videoImps[validBidRequest.transactionId] = {}; + } + if (!videoImps[validBidRequest.transactionId].hasOwnProperty('ixImps')) { + videoImps[validBidRequest.transactionId].ixImps = []; + } + + videoImps[validBidRequest.transactionId].ixImps.push(bidToVideoImp(validBidRequest)); } else { utils.logError('Bid size is not included in video playerSize') } @@ -516,27 +638,47 @@ export const spec = { if (validBidRequest.mediaType === BANNER || utils.deepAccess(validBidRequest, 'mediaTypes.banner') || (!validBidRequest.mediaType && !validBidRequest.mediaTypes)) { let imp = bidToBannerImp(validBidRequest); - bannerImps.push(imp); - updateMissingSizes(validBidRequest, missingBannerSizes, imp); + + if (!bannerImps.hasOwnProperty(validBidRequest.transactionId)) { + bannerImps[validBidRequest.transactionId] = {}; + } + if (!bannerImps[validBidRequest.transactionId].hasOwnProperty('ixImps')) { + bannerImps[validBidRequest.transactionId].ixImps = [] + } + bannerImps[validBidRequest.transactionId].ixImps.push(imp); + if (ixConfig.hasOwnProperty('detectMissingSizes') && ixConfig.detectMissingSizes) { + updateMissingSizes(validBidRequest, missingBannerSizes, imp); + } } } - // Finding the missing banner sizes ,and making impressions for them - for (var transactionID in missingBannerSizes) { - if (missingBannerSizes.hasOwnProperty(transactionID)) { - let missingSizes = missingBannerSizes[transactionID].missingSizes; + + // Finding the missing banner sizes, and making impressions for them + for (var transactionId in missingBannerSizes) { + if (missingBannerSizes.hasOwnProperty(transactionId)) { + let missingSizes = missingBannerSizes[transactionId].missingSizes; + + if (!bannerImps.hasOwnProperty(transactionId)) { + bannerImps[transactionId] = {}; + } + if (!bannerImps[transactionId].hasOwnProperty('missingImps')) { + bannerImps[transactionId].missingImps = []; + bannerImps[transactionId].missingCount = 0; + } + + let origImp = missingBannerSizes[transactionId].impression; for (let i = 0; i < missingSizes.length; i++) { - let origImp = missingBannerSizes[transactionID].impression; let newImp = createMissingBannerImp(origImp, missingSizes[i]); - bannerImps.push(newImp); + bannerImps[transactionId].missingImps.push(newImp); + bannerImps[transactionId].missingCount++; } } } - if (bannerImps.length > 0) { - reqs.push(buildRequest(validBidRequests, bidderRequest, bannerImps, BANNER_ENDPOINT_VERSION)); + if (Object.keys(bannerImps).length > 0) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, bannerImps, BANNER_ENDPOINT_VERSION)); } - if (videoImps.length > 0) { - reqs.push(buildRequest(validBidRequests, bidderRequest, videoImps, VIDEO_ENDPOINT_VERSION)); + if (Object.keys(videoImps).length > 0) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, videoImps, VIDEO_ENDPOINT_VERSION)); } return reqs; diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index b5cb0d9d2c1..5b9903c91d2 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -288,6 +288,26 @@ pbjs.setConfig({ } }); ``` +#### The **detectMissingSizes** feature +By default, the IX bidding adapter bids on all banner sizes available in the ad unit when configured to at least one banner size. If you want the IX bidding adapter to only bid on the banner size it’s configured to, switch off this feature using `detectMissingSizes`. +``` +pbjs.setConfig({ + ix: { + detectMissingSizes: false + } + }); +``` +OR +``` +pbjs.setBidderConfig({ + bidders: ["ix"], + config: { + ix: { + detectMissingSizes: false + } + } + }); +``` ### 2. Include `ixBidAdapter` in your build process diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index b7c8879ed8e..197c3c192c8 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -14,11 +14,12 @@ 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'; +import { getGlobal } from '../src/prebidGlobal.js'; const SUBMODULE_NAME = 'jwplayer'; -let requestCount = 0; -let requestTimeout = 150; const segCache = {}; +const pendingRequests = {}; +let activeRequestCount = 0; let resumeBidRequest; /** @type {RtdSubmodule} */ @@ -29,12 +30,12 @@ export const jwplayerSubmodule = { */ name: SUBMODULE_NAME, /** - * get data and send back to realTimeData module + * add targeting data to bids and signal completion to realTimeData module * @function - * @param {adUnit[]} adUnits + * @param {Obj} bidReqConfig * @param {function} onDone */ - getData: getSegments, + getBidRequestData: enrichBidRequest, init }; @@ -45,14 +46,12 @@ config.getConfig('realTimeData', ({realTimeData}) => { 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) { +function init(provider, userConsent) { return true; } @@ -67,40 +66,57 @@ export function fetchTargetingInformation(jwTargeting) { } export function fetchTargetingForMediaId(mediaId) { - const ajax = ajaxBuilder(requestTimeout); - requestCount++; + const ajax = ajaxBuilder(); + // TODO: Avoid checking undefined vs null by setting a callback to pendingRequests. + pendingRequests[mediaId] = null; 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(); + const segment = parseSegment(response); + cacheSegments(segment, mediaId); + onRequestCompleted(mediaId, !!segment); }, error: function () { logError('failed to retrieve targeting information'); - onRequestCompleted(); + onRequestCompleted(mediaId, false); } }); } -function onRequestCompleted() { - requestCount--; - if (requestCount > 0) { +function parseSegment(response) { + let segment; + try { + const data = JSON.parse(response); + if (!data) { + throw ('Empty response'); + } + + const playlist = data.playlist; + if (!playlist || !playlist.length) { + throw ('Empty playlist'); + } + + segment = playlist[0].jwpseg; + } catch (err) { + logError(err); + } + return segment; +} + +function cacheSegments(jwpseg, mediaId) { + if (jwpseg && mediaId) { + segCache[mediaId] = jwpseg; + } +} + +function onRequestCompleted(mediaID, success) { + const callback = pendingRequests[mediaID]; + if (callback) { + callback(success ? getVatFromCache(mediaID) : { mediaID }); + activeRequestCount--; + } + delete pendingRequests[mediaID]; + + if (activeRequestCount > 0) { return; } @@ -110,67 +126,86 @@ function onRequestCompleted() { } } -function getSegments(adUnits, onDone) { - executeAfterPrefetch(() => { - const realTimeData = adUnits.reduce((data, adUnit) => { - const code = adUnit.code; - const vat = code && getTargetingForBid(adUnit); +function enrichBidRequest(bidReqConfig, onDone) { + activeRequestCount = 0; + const adUnits = bidReqConfig.adUnits || getGlobal().adUnits; + enrichAdUnits(adUnits); + if (activeRequestCount <= 0) { + onDone(); + } else { + resumeBidRequest = onDone; + } +} + +/** + * get targeting data and write to bids + * @function + * @param {adUnit[]} adUnits + * @param {function} onDone + */ +export function enrichAdUnits(adUnits) { + const fpdFallback = config.getConfig('fpd.context.data.jwTargeting'); + adUnits.forEach(adUnit => { + const onVatResponse = function (vat) { if (!vat) { - return data; + return; } + const targeting = formatTargetingResponse(vat); + addTargetingToBids(adUnit.bids, targeting); + }; - const { segments, mediaID } = vat; - const jwTargeting = {}; - if (segments && segments.length) { - jwTargeting.segments = segments; - } + const jwTargeting = extractPublisherParams(adUnit, fpdFallback); + loadVat(jwTargeting, onVatResponse); + }); +} - if (mediaID) { - const id = 'jw_' + mediaID; - jwTargeting.content = { - id - } - } +export function extractPublisherParams(adUnit, fallback) { + let adUnitTargeting; + try { + adUnitTargeting = adUnit.fpd.context.data.jwTargeting; + } catch (e) {} + return Object.assign({}, fallback, adUnitTargeting); +} - data[code] = { - jwTargeting - }; - return data; - }, {}); - onDone(realTimeData); - }); +function loadVat(params, onCompletion) { + if (!params || !Object.keys(params).length) { + return; + } + + const { playerID, mediaID } = params; + if (pendingRequests[mediaID] !== undefined) { + loadVatForPendingRequest(playerID, mediaID, onCompletion); + return; + } + + const vat = getVatFromCache(mediaID) || getVatFromPlayer(playerID, mediaID) || { mediaID }; + onCompletion(vat); } -function executeAfterPrefetch(callback) { - if (requestCount > 0) { - resumeBidRequest = callback; +function loadVatForPendingRequest(playerID, mediaID, callback) { + const vat = getVatFromPlayer(playerID, mediaID); + if (vat) { + callback(vat); } else { - callback(); + activeRequestCount++; + pendingRequests[mediaID] = 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) { +export function getVatFromCache(mediaID) { + const segments = segCache[mediaID]; + + if (!segments) { return null; } - const playerID = jwTargeting.playerID; - let mediaID = jwTargeting.mediaID; - let segments = segCache[mediaID]; - if (segments) { - return { - segments, - mediaID - }; - } + return { + segments, + mediaID + }; +} + +export function getVatFromPlayer(playerID, mediaID) { const player = getPlayer(playerID); if (!player) { return null; @@ -182,10 +217,8 @@ export function getTargetingForBid(bidRequest) { } mediaID = mediaID || item.mediaid; - segments = item.jwpseg; - if (segments && mediaID) { - segCache[mediaID] = segments; - } + const segments = item.jwpseg; + cacheSegments(segments, mediaID) return { segments, @@ -193,6 +226,37 @@ export function getTargetingForBid(bidRequest) { }; } +export function formatTargetingResponse(vat) { + const { segments, mediaID } = vat; + const targeting = {}; + if (segments && segments.length) { + targeting.segments = segments; + } + + if (mediaID) { + const id = 'jw_' + mediaID; + targeting.content = { + id + } + } + return targeting; +} + +function addTargetingToBids(bids, targeting) { + if (!bids || !targeting) { + return; + } + + bids.forEach(bid => addTargetingToBid(bid, targeting)); +} + +export function addTargetingToBid(bid, targeting) { + const rtd = bid.rtd || {}; + const jwRtd = {}; + jwRtd[SUBMODULE_NAME] = Object.assign({}, rtd[SUBMODULE_NAME], { targeting }); + bid.rtd = Object.assign({}, rtd, jwRtd); +} + function getPlayer(playerID) { const jwplayer = window.jwplayer; if (!jwplayer) { diff --git a/modules/jwplayerRtdProvider.md b/modules/jwplayerRtdProvider.md index 06a7f69f497..ae09277979a 100644 --- a/modules/jwplayerRtdProvider.md +++ b/modules/jwplayerRtdProvider.md @@ -1,8 +1,8 @@ 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 +having to integrate with the Player Bidding product. This prebid module makes JW Player's video ad targeting information accessible to Bid Adapters. -**Usage for Publishers:** +#Usage for Publishers: Compile the JW Player RTD Provider into your Prebid build: @@ -25,26 +25,22 @@ pbjs.setConfig({ } }); ``` - -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: +Lastly, include the content's media ID and/or the player's ID in the matching AdUnit's `fpd.context.data`: ```javascript const adUnit = { code: '/19968336/prebid_native_example_1', ... - jwTargeting: { - playerID: 'abcd', - mediaID: '1234' + fpd: { + context: { + data: { + jwTargeting: { + // Note: the following Ids are placeholders and should be replaced with your Ids. + playerID: 'abcd', + mediaID: '1234' + } + } + } } }; @@ -56,25 +52,50 @@ pbjs.que.push(function() { }); ``` -**Usage for Bid Adapters:** +**Note**: You may also include `jwTargeting` information in the prebid config's `fpd.context.data`. Information provided in the adUnit will always supersede, and information in the config will be used as a fallback. + +##Prefetching +In order to prefetch targeting information for certain media, include the media IDs in the `jwplayerDataProvider` var and set `waitForIt` to `true`: + +```javascript +const jwplayerDataProvider = { + name: "jwplayer", + waitForIt: true, + params: { + mediaIDs: ['abc', 'def', 'ghi', 'jkl'] + } +}; +``` + +You must also set a value to `auctionDelay` in the config's `realTimeData` object + +```javascript +realTimeData = { + auctionDelay: 100, + ... +}; +``` + +#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' - } - } - } + adUnitCode: 'xyz', + bidId: 'abc', + ..., + rtd: { + jwplayer: { + targeting: { + segments: ['123', '456'], + content: { + id: 'jw_abc123' + } + } + } + } } ``` @@ -94,3 +115,5 @@ To view an example: - in your browser, navigate to: `http://localhost:9999/integrationExamples/gpt/jwplayerRtdProvider_example.html` + +**Note:** the mediaIds in the example are placeholder values; replace them with your existing IDs. diff --git a/modules/krushmediaBidAdapter.js b/modules/krushmediaBidAdapter.js new file mode 100644 index 00000000000..de1cce503e3 --- /dev/null +++ b/modules/krushmediaBidAdapter.js @@ -0,0 +1,123 @@ +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 = 'krushmedia'; +const AD_URL = 'https://ads4.krushmedia.com/?c=rtb&m=hb'; +const SYNC_URL = 'https://cs.krushmedia.com/html?src=pbjs' + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency) { + return false; + } + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers); + default: + return false; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.key))); + }, + + 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); + }; + + const placements = []; + const request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + 'secure': 1, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent + } + } + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const placement = { + key: bid.params.key, + bidId: bid.bidId, + traffic: bid.params.traffic || BANNER, + schain: bid.schain || {}, + }; + + if (bid.mediaTypes && bid.mediaTypes[BANNER] && bid.mediaTypes[BANNER].sizes) { + placement.sizes = bid.mediaTypes[BANNER].sizes; + } else if (bid.mediaTypes && bid.mediaTypes[VIDEO] && bid.mediaTypes[VIDEO].playerSize) { + placement.wPlayer = bid.mediaTypes[VIDEO].playerSize[0]; + placement.hPlayer = bid.mediaTypes[VIDEO].playerSize[1]; + } else if (bid.mediaTypes && bid.mediaTypes[NATIVE]) { + placement.native = bid.mediaTypes[NATIVE]; + } + placements.push(placement); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncUrl = SYNC_URL + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + return [{ + type: 'iframe', + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/krushmediaBidAdapter.md b/modules/krushmediaBidAdapter.md new file mode 100644 index 00000000000..7bf7c4fe491 --- /dev/null +++ b/modules/krushmediaBidAdapter.md @@ -0,0 +1,80 @@ +# Overview + +``` +Module Name: krushmedia Bidder Adapter +Module Type: krushmedia Bidder Adapter +Maintainer: adapter@krushmedia.com +``` + +# Description + +Module that connects to krushmedia demand sources + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'krushmedia', + params: { + key: 0, + traffic: 'banner' + } + } + ] + }, + // Will return test vast xml. All video params are stored under placement in publishers UI + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'krushmedia', + params: { + key: 0, + traffic: 'video' + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'krushmedia', + params: { + key: 0, + traffic: 'native' + } + } + ] + } + ]; +``` diff --git a/modules/lemmaBidAdapter.js b/modules/lemmaBidAdapter.js index 1ad660e5916..5941802f97d 100644 --- a/modules/lemmaBidAdapter.js +++ b/modules/lemmaBidAdapter.js @@ -5,10 +5,13 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; var BIDDER_CODE = 'lemma'; var LOG_WARN_PREFIX = 'LEMMA: '; var ENDPOINT = 'https://ads.lemmatechnologies.com/lemma/servad'; +var USER_SYNC = 'https://sync.lemmatechnologies.com/js/usersync.html?'; var DEFAULT_CURRENCY = 'USD'; var AUCTION_TYPE = 2; var DEFAULT_TMAX = 300; var DEFAULT_NET_REVENUE = false; +var pubId = 0; +var adunitId = 0; export var spec = { @@ -57,6 +60,29 @@ export var spec = { interpretResponse: (response, request) => { return parseRTBResponse(request, response.body); }, + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { + let syncurl = USER_SYNC + 'pid=' + pubId; + + // Attaching GDPR Consent Params in UserSync url + if (gdprConsent) { + syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + + // CCPA + if (uspConsent) { + syncurl += '&us_privacy=' + encodeURIComponent(uspConsent); + } + + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: syncurl + }]; + } else { + utils.logWarn(LOG_WARN_PREFIX + 'Please enable iframe based user sync.'); + } + }, }; function _initConf(refererInfo) { @@ -167,8 +193,8 @@ function _getImpressionArray(request) { function endPointURL(request) { var params = request && request[0].params ? request[0].params : null; if (params) { - var pubId = params.pubId ? params.pubId : 0; - var adunitId = params.adunitId ? params.adunitId : 0; + pubId = params.pubId ? params.pubId : 0; + adunitId = params.adunitId ? params.adunitId : 0; return ENDPOINT + '?pid=' + pubId + '&aid=' + adunitId; } return null; @@ -183,7 +209,7 @@ function _getDomain(url) { function _getSiteObject(request, conf) { var params = request && request.params ? request.params : null; if (params) { - var pubId = params.pubId ? params.pubId : '0'; + pubId = params.pubId ? params.pubId : '0'; var siteId = params.siteId ? params.siteId : '0'; var appParams = params.app; if (!appParams) { @@ -204,7 +230,7 @@ function _getSiteObject(request, conf) { function _getAppObject(request) { var params = request && request.params ? request.params : null; if (params) { - var pubId = params.pubId ? params.pubId : 0; + pubId = params.pubId ? params.pubId : 0; var appParams = params.app; if (appParams) { return { diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 7981b62dc51..4f18c73ad2a 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -5,14 +5,32 @@ * @requires module:modules/userId */ import * as utils from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; +import { triggerPixel } from '../src/utils.js'; +import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { LiveConnect } from 'live-connect-js/cjs/live-connect.js'; -import { uspDataHandler } from '../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; import { getStorageManager } from '../src/storageManager.js'; const MODULE_NAME = 'liveIntentId'; export const storage = getStorageManager(null, MODULE_NAME); +const calls = { + ajaxGet: (url, onSuccess, onError, timeout) => { + ajaxBuilder(timeout)( + url, + { + success: onSuccess, + error: onError + }, + undefined, + { + method: 'GET', + withCredentials: true + } + ) + }, + pixelGet: (url, onload) => triggerPixel(url, onload) +} let eventFired = false; let liveConnect = null; @@ -64,18 +82,30 @@ function initializeLiveConnect(configParams) { if (configParams.partner) { identityResolutionConfig.source = configParams.partner } + if (configParams.ajaxTimeout) { + identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout; + } const liveConnectConfig = parseLiveIntentCollectorConfig(configParams.liCollectConfig); liveConnectConfig.wrapperName = 'prebid'; liveConnectConfig.identityResolutionConfig = identityResolutionConfig; liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; + if (configParams.emailHash) { + liveConnectConfig.eventSource = { hash: configParams.emailHash } + } const usPrivacyString = uspDataHandler.getConsentData(); if (usPrivacyString) { liveConnectConfig.usPrivacyString = usPrivacyString; } + const gdprConsent = gdprDataHandler.getConsentData() + if (gdprConsent) { + liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; + liveConnectConfig.gdprConsent = gdprConsent.consentString; + } - // The second param is the storage object, which means that all LS & Cookie manipulation will go through PBJS utils. - liveConnect = LiveConnect(liveConnectConfig, storage); + // The second param is the storage object, LS & Cookie manipulation uses PBJS utils. + // The third param is the ajax and pixel object, the ajax and pixel use PBJS utils. + liveConnect = LiveConnect(liveConnectConfig, storage, calls); return liveConnect; } @@ -132,11 +162,9 @@ export const liveIntentIdSubmodule = { return; } tryFireEvent(); - // Don't do the internal ajax call, but use the composed url and fire it via PBJS ajax module - const url = liveConnect.resolutionCallUrl(); - const result = function (callback) { - const callbacks = { - success: response => { + const result = function(callback) { + liveConnect.resolve( + response => { let responseObj = {}; if (response) { try { @@ -147,14 +175,14 @@ export const liveIntentIdSubmodule = { } callback(responseObj); }, - error: error => { + error => { utils.logError(`${MODULE_NAME}: ID fetch encountered an error: `, error); callback(); } - }; - ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); - }; - return {callback: result}; + ) + } + + return { callback: result }; } }; diff --git a/modules/livewrappedAnalyticsAdapter.js b/modules/livewrappedAnalyticsAdapter.js index 9f571cb5ae0..a872a709ec9 100644 --- a/modules/livewrappedAnalyticsAdapter.js +++ b/modules/livewrappedAnalyticsAdapter.js @@ -36,6 +36,15 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE args.bids.forEach(function(bidRequest) { cache.auctions[args.auctionId].gdprApplies = args.gdprConsent ? args.gdprConsent.gdprApplies : undefined; cache.auctions[args.auctionId].gdprConsent = args.gdprConsent ? args.gdprConsent.consentString : undefined; + let lwFloor; + + if (bidRequest.lwflr) { + lwFloor = bidRequest.lwflr.flr; + + let buyerFloor = bidRequest.lwflr.bflrs ? bidRequest.lwflr.bflrs[bidRequest.bidder] : undefined; + + lwFloor = buyerFloor || lwFloor; + } cache.auctions[args.auctionId].bids[bidRequest.bidId] = { bidder: bidRequest.bidder, @@ -45,7 +54,11 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE timeout: false, sendStatus: 0, readyToSend: 0, - start: args.start + start: args.start, + lwFloor: lwFloor, + floorData: bidRequest.floorData, + auc: bidRequest.auc, + buc: bidRequest.buc } utils.logInfo(bidRequest); @@ -123,10 +136,11 @@ livewrappedAnalyticsAdapter.sendEvents = function() { var events = { publisherId: initOptions.publisherId, gdpr: sentRequests.gdpr, + auctionIds: sentRequests.auctionIds, requests: sentRequests.sentRequests, - responses: getResponses(), - wins: getWins(), - timeouts: getTimeouts(), + responses: getResponses(sentRequests.gdpr, sentRequests.auctionIds), + wins: getWins(sentRequests.gdpr, sentRequests.auctionIds), + timeouts: getTimeouts(sentRequests.auctionIds), bidAdUnits: getbidAdUnits(), rcv: getAdblockerRecovered() }; @@ -150,20 +164,12 @@ function getAdblockerRecovered() { function getSentRequests() { var sentRequests = []; var gdpr = []; + var auctionIds = []; Object.keys(cache.auctions).forEach(auctionId => { let auction = cache.auctions[auctionId]; - var gdprPos = 0; - for (gdprPos = 0; gdprPos < gdpr.length; gdprPos++) { - if (gdpr[gdprPos].gdprApplies == auction.gdprApplies && - gdpr[gdprPos].gdprConsent == auction.gdprConsent) { - break; - } - } - - if (gdprPos == gdpr.length) { - gdpr[gdprPos] = {gdprApplies: auction.gdprApplies, gdprConsent: auction.gdprConsent}; - } + let gdprPos = getGdprPos(gdpr, auction); + let auctionIdPos = getAuctionIdPos(auctionIds, auctionId); Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { let bid = auction.bids[bidId]; @@ -174,21 +180,27 @@ function getSentRequests() { timeStamp: auction.timeStamp, adUnit: bid.adUnit, bidder: bid.bidder, - gdpr: gdprPos + gdpr: gdprPos, + floor: bid.lwFloor, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc }); } }); }); - return {gdpr: gdpr, sentRequests: sentRequests}; + return {gdpr: gdpr, auctionIds: auctionIds, sentRequests: sentRequests}; } -function getResponses() { +function getResponses(gdpr, auctionIds) { var responses = []; Object.keys(cache.auctions).forEach(auctionId => { Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { let auction = cache.auctions[auctionId]; + let gdprPos = getGdprPos(gdpr, auction); + let auctionIdPos = getAuctionIdPos(auctionIds, auctionId) let bid = auction.bids[bidId]; if (bid.readyToSend && !(bid.sendStatus & RESPONSESENT) && !bid.timeout) { bid.sendStatus |= RESPONSESENT; @@ -202,7 +214,13 @@ function getResponses() { cpm: bid.cpm, ttr: bid.ttr, IsBid: bid.isBid, - mediaType: bid.mediaType + mediaType: bid.mediaType, + gdpr: gdprPos, + floor: bid.floorData ? bid.floorData.floorValue : bid.lwFloor, + floorCur: bid.floorData ? bid.floorData.floorCurrency : undefined, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc }); } }); @@ -211,13 +229,16 @@ function getResponses() { return responses; } -function getWins() { +function getWins(gdpr, auctionIds) { var wins = []; Object.keys(cache.auctions).forEach(auctionId => { Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { let auction = cache.auctions[auctionId]; + let gdprPos = getGdprPos(gdpr, auction); + let auctionIdPos = getAuctionIdPos(auctionIds, auctionId); let bid = auction.bids[bidId]; + if (!(bid.sendStatus & WINSENT) && bid.won) { bid.sendStatus |= WINSENT; @@ -228,7 +249,13 @@ function getWins() { width: bid.width, height: bid.height, cpm: bid.cpm, - mediaType: bid.mediaType + mediaType: bid.mediaType, + gdpr: gdprPos, + floor: bid.floorData ? bid.floorData.floorValue : bid.lwFloor, + floorCur: bid.floorData ? bid.floorData.floorCurrency : undefined, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc }); } }); @@ -237,10 +264,42 @@ function getWins() { return wins; } -function getTimeouts() { +function getGdprPos(gdpr, auction) { + var gdprPos = 0; + for (gdprPos = 0; gdprPos < gdpr.length; gdprPos++) { + if (gdpr[gdprPos].gdprApplies == auction.gdprApplies && + gdpr[gdprPos].gdprConsent == auction.gdprConsent) { + break; + } + } + + if (gdprPos == gdpr.length) { + gdpr[gdprPos] = {gdprApplies: auction.gdprApplies, gdprConsent: auction.gdprConsent}; + } + + return gdprPos; +} + +function getAuctionIdPos(auctionIds, auctionId) { + var auctionIdPos = 0; + for (auctionIdPos = 0; auctionIdPos < auctionIds.length; auctionIdPos++) { + if (auctionIds[auctionIdPos] == auctionId) { + break; + } + } + + if (auctionIdPos == auctionIds.length) { + auctionIds[auctionIdPos] = auctionId; + } + + return auctionIdPos; +} + +function getTimeouts(auctionIds) { var timeouts = []; Object.keys(cache.auctions).forEach(auctionId => { + let auctionIdPos = getAuctionIdPos(auctionIds, auctionId); Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { let auction = cache.auctions[auctionId]; let bid = auction.bids[bidId]; @@ -250,7 +309,10 @@ function getTimeouts() { timeouts.push({ bidder: bid.bidder, adUnit: bid.adUnit, - timeStamp: auction.timeStamp + timeStamp: auction.timeStamp, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc }); } }); diff --git a/modules/livewrappedAnalyticsAdapter.md b/modules/livewrappedAnalyticsAdapter.md new file mode 100644 index 00000000000..de4f352aa19 --- /dev/null +++ b/modules/livewrappedAnalyticsAdapter.md @@ -0,0 +1,22 @@ +# Overview +Module Name: Livewrapped Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: info@livewrapped.com + +# Description + +Analytics adapter for Livewrapped AB. In order to use the adapter, please contact Livewrapped AB. + +# Test Parameters + +``` +{ + provider: 'livewrapped', + options : { + publisherId: "64c01620-fa98-4936-9794-6001d8ebfdb0" + } +} + +``` diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js index 0a5464fd21f..512fc775d95 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -221,6 +221,10 @@ function bidToAdRequest(bid) { options: bid.params.options }; + if (bid.auc !== undefined) { + adRequest.auc = bid.auc; + } + adRequest.native = utils.deepAccess(bid, 'mediaTypes.native'); adRequest.video = utils.deepAccess(bid, 'mediaTypes.video'); @@ -276,8 +280,9 @@ function handleEids(bidRequests) { let eids = []; const bidRequest = bidRequests[0]; if (bidRequest && bidRequest.userId) { - AddExternalUserId(eids, utils.deepAccess(bidRequest, `userId.pubcid`), 'pubcommon', 1); // Also add this to eids + AddExternalUserId(eids, utils.deepAccess(bidRequest, `userId.pubcid`), 'pubcid.org', 1); // Also add this to eids AddExternalUserId(eids, utils.deepAccess(bidRequest, `userId.id5id.uid`), 'id5-sync.com', 1); + AddExternalUserId(eids, utils.deepAccess(bidRequest, `userId.criteoId`), 'criteo.com', 1); } if (eids.length > 0) { return {user: {ext: {eids}}}; diff --git a/modules/livewrappedBidAdapter.md b/modules/livewrappedBidAdapter.md index c5d867af8fe..10fc2a4778a 100644 --- a/modules/livewrappedBidAdapter.md +++ b/modules/livewrappedBidAdapter.md @@ -20,7 +20,7 @@ var adUnits = [ bids: [{ bidder: 'livewrapped', params: { - adUnitId: '6A32352E-BC17-4B94-B2A7-5BF1724417D7' + adUnitId: 'D801852A-681F-11E8-86A7-0A44794250D4' } }] } diff --git a/modules/lkqdBidAdapter.js b/modules/lkqdBidAdapter.js index 51d5c48e1fc..0f5782649ad 100644 --- a/modules/lkqdBidAdapter.js +++ b/modules/lkqdBidAdapter.js @@ -1,6 +1,7 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'lkqd'; const BID_TTL_DEFAULT = 300; @@ -148,6 +149,9 @@ function buildRequests(validBidRequests, bidderRequest) { if (bidRequest.params.hasOwnProperty('dnt') && bidRequest.params.dnt != null) { sspData.dnt = bidRequest.params.dnt; } + if (config.getConfig('coppa') === true) { + sspData.coppa = 1; + } if (bidRequest.params.hasOwnProperty('pageurl') && bidRequest.params.pageurl != null) { sspData.pageurl = bidRequest.params.pageurl; } else if (bidderRequest && bidderRequest.refererInfo) { @@ -177,6 +181,12 @@ function buildRequests(validBidRequests, bidderRequest) { sspData.bidWidth = playerWidth; sspData.bidHeight = playerHeight; + for (let k = 1; k <= 40; k++) { + if (bidRequest.params.hasOwnProperty(`c${k}`) && bidRequest.params[`c${k}`]) { + sspData[`c${k}`] = bidRequest.params[`c${k}`]; + } + } + bidRequests.push({ method: 'GET', url: sspUrl, diff --git a/modules/lunamediahbBidAdapter.js b/modules/lunamediahbBidAdapter.js new file mode 100644 index 00000000000..1376d0c1714 --- /dev/null +++ b/modules/lunamediahbBidAdapter.js @@ -0,0 +1,107 @@ +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 = 'lunamediahb'; +const AD_URL = 'https://balancer.lmgssp.com/?c=o&m=multi'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency) { + return false; + } + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers); + default: + return false; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.placementId))); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + let winTop = window; + let location; + try { + location = new URL(bidderRequest.refererInfo.referer) + winTop = window.top; + } catch (e) { + location = winTop.location; + utils.logMessage(e); + }; + + const placements = []; + const request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + 'secure': 1, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent + } + } + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const placement = { + placementId: bid.params.placementId, + bidId: bid.bidId, + schain: bid.schain || {}, + }; + const mediaType = bid.mediaTypes + + if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { + placement.sizes = mediaType[BANNER].sizes; + placement.traffic = BANNER; + } else if (mediaType && mediaType[VIDEO] && mediaType[VIDEO].playerSize) { + placement.wPlayer = mediaType[VIDEO].playerSize[0]; + placement.hPlayer = mediaType[VIDEO].playerSize[1]; + placement.traffic = VIDEO; + } else if (mediaType && mediaType[NATIVE]) { + placement.native = mediaType[NATIVE]; + placement.traffic = NATIVE; + } + placements.push(placement); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + response.push(resItem); + } + } + return response; + }, +}; + +registerBidder(spec); diff --git a/modules/lunamediahbBidAdapter.md b/modules/lunamediahbBidAdapter.md new file mode 100644 index 00000000000..184dd846a9d --- /dev/null +++ b/modules/lunamediahbBidAdapter.md @@ -0,0 +1,72 @@ +# Overview + +``` +Module Name: lunamedia Bidder Adapter +Module Type: lunamedia Bidder Adapter +Maintainer: support@lunamedia.io +``` + +# Description + +Module that connects to lunamedia demand sources + +# Test Parameters +``` + var adUnits = [ + { + code:'1', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'lunamediahb', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids:[ + { + bidder: 'lunamediahb', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + native: { + title: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids:[ + { + bidder: 'lunamediahb', + params: { + placementId: 0 + } + } + ] + } + ]; +``` diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index 49e2bdc225f..a2dc8bdfd03 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -3,9 +3,11 @@ import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { getRefererInfo } from '../src/refererDetection.js'; +import { Renderer } from '../src/Renderer.js'; const BIDDER_CODE = 'medianet'; const BID_URL = 'https://prebid.media.net/rtb/prebid'; +const PLAYER_URL = 'https://prebid.media.net/video/bundle.js'; const SLOT_VISIBILITY = { NOT_DETERMINED: 0, ABOVE_THE_FOLD: 1, @@ -16,10 +18,14 @@ const EVENTS = { BID_WON_EVENT_NAME: 'client_bid_won' }; const EVENT_PIXEL_URL = 'qsearch-a.akamaihd.net/log'; - +const OUTSTREAM = 'outstream'; let refererInfo = getRefererInfo(); let mnData = {}; + +window.mnet = window.mnet || {}; +window.mnet.queue = window.mnet.queue || []; + mnData.urlData = { domain: utils.parseUrl(refererInfo.referer).hostname, page: refererInfo.referer, @@ -198,7 +204,7 @@ function slotParams(bidRequest) { params.tagid = bidRequest.params.crid.toString(); } - let bidFloor = parseFloat(bidRequest.params.bidfloor); + let bidFloor = parseFloat(bidRequest.params.bidfloor || bidRequest.params.bidFloor); if (bidFloor) { params.bidfloor = bidFloor; } @@ -321,6 +327,40 @@ function clearMnData() { mnData = {}; } +function addRenderer(bid) { + const videoContext = utils.deepAccess(bid, 'context') || ''; + const vastTimeout = utils.deepAccess(bid, 'vto'); + /* Adding renderer only when the context is Outstream + and the provider has responded with a renderer. + */ + if (videoContext == OUTSTREAM && vastTimeout) { + bid.renderer = newVideoRenderer(bid); + } +} + +function newVideoRenderer(bid) { + const renderer = Renderer.install({ + url: PLAYER_URL, + }); + renderer.setRender(function (bid) { + window.mnet.queue.push(function () { + const obj = { + width: bid.width, + height: bid.height, + vastTimeout: bid.vto, + maxAllowedVastTagRedirects: bid.mavtr, + allowVpaid: bid.avp, + autoPlay: bid.ap, + preload: bid.pl, + mute: bid.mt + } + const adUnitCode = bid.dfp_id; + const divId = utils.getGptSlotInfoForAdUnitCode(adUnitCode).divId || adUnitCode; + window.mnet.mediaNetoutstreamPlayer(bid, divId, obj); + }); + }); + return renderer; +} export const spec = { code: BIDDER_CODE, @@ -387,9 +427,10 @@ export const spec = { } validBids = bids.filter(bid => isValidBid(bid)); + validBids.forEach(addRenderer); + return validBids; }, - getUserSyncs: function(syncOptions, serverResponses) { let cookieSyncUrls = fetchCookieSyncUrls(serverResponses); diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 288526d3cc5..87f768d3b77 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -1,7 +1,7 @@ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = 'mediasquare'; const BIDDER_URL_PROD = 'https://pbs-front.mediasquare.fr/' @@ -13,7 +13,7 @@ const BIDDER_ENDPOINT_WINNING = 'winning'; export const spec = { code: BIDDER_CODE, aliases: ['msq'], // short code - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, NATIVE, VIDEO], /** * Determines whether or not the given bid request is valid. * @@ -99,6 +99,14 @@ export const spec = { 'code': value['code'] } }; + if ('native' in value) { + bidResponse['native'] = value['native']; + bidResponse['mediaType'] = 'native'; + } else if ('video' in value) { + if ('url' in value['video']) { bidResponse['vastUrl'] = value['video']['url'] } + if ('xml' in value['video']) { bidResponse['vastXml'] = value['video']['xml'] } + bidResponse['mediaType'] = 'video'; + } if (value.hasOwnProperty('deal_id')) { bidResponse['dealId'] = value['deal_id']; } bidResponses.push(bidResponse); }); @@ -136,7 +144,7 @@ export const spec = { // fires a pixel to confirm a winning bid let params = []; let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD; - let paramsToSearchFor = ['cpm', 'size', 'mediaType', 'currency', 'creativeId', 'adUnitCode', 'timeToRespond', 'auctionId', 'requestId'] + let paramsToSearchFor = ['cpm', 'size', 'mediaType', 'currency', 'creativeId', 'adUnitCode', 'timeToRespond'] if (bid.hasOwnProperty('mediasquare')) { if (bid['mediasquare'].hasOwnProperty('bidder')) { params.push('bidder=' + bid['mediasquare']['bidder']); } if (bid['mediasquare'].hasOwnProperty('code')) { params.push('code=' + bid['mediasquare']['code']); } diff --git a/modules/mobfoxpbBidAdapter.js b/modules/mobfoxpbBidAdapter.js new file mode 100644 index 00000000000..c7e96b95179 --- /dev/null +++ b/modules/mobfoxpbBidAdapter.js @@ -0,0 +1,99 @@ +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 = 'mobfoxpb'; +const AD_URL = 'https://bes.mobfox.com/?c=o&m=multi'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency) { + return false; + } + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers); + default: + return false; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.placementId))); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + const winTop = utils.getWindowTop(); + const location = winTop.location; + const placements = []; + const request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + 'secure': 1, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent + } + } + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const placement = { + placementId: bid.params.placementId, + bidId: bid.bidId, + schain: bid.schain || {}, + }; + const mediaType = bid.mediaTypes + + if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { + placement.sizes = mediaType[BANNER].sizes; + placement.traffic = BANNER; + } else if (mediaType && mediaType[VIDEO] && mediaType[VIDEO].playerSize) { + placement.wPlayer = mediaType[VIDEO].playerSize[0]; + placement.hPlayer = mediaType[VIDEO].playerSize[1]; + placement.traffic = VIDEO; + } else if (mediaType && mediaType[NATIVE]) { + placement.native = mediaType[NATIVE]; + placement.traffic = NATIVE; + } + placements.push(placement); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + response.push(resItem); + } + } + return response; + }, +}; + +registerBidder(spec); diff --git a/modules/mobfoxpbBidAdapter.md b/modules/mobfoxpbBidAdapter.md new file mode 100644 index 00000000000..6eb549919d7 --- /dev/null +++ b/modules/mobfoxpbBidAdapter.md @@ -0,0 +1,72 @@ +# Overview + +``` +Module Name: mobfox Bidder Adapter +Module Type: mobfox Bidder Adapter +Maintainer: platform@mobfox.com +``` + +# Description + +Module that connects to mobfox demand sources + +# Test Parameters +``` + var adUnits = [ + { + code:'1', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'mobfoxpb', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids:[ + { + bidder: 'mobfoxpb', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + native: { + title: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids:[ + { + bidder: 'mobfoxpb', + params: { + placementId: 0 + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index 00cb14dc01d..051202cab97 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -6,7 +6,7 @@ import { getStorageManager } from '../src/storageManager.js'; const storage = getStorageManager(); const BIDDER_CODE = 'nobid'; -window.nobidVersion = '1.2.8'; +window.nobidVersion = '1.2.9'; window.nobid = window.nobid || {}; window.nobid.bidResponses = window.nobid.bidResponses || {}; window.nobid.timeoutTotal = 0; @@ -114,6 +114,23 @@ function nobidBuildRequests(bids, bidderRequest) { utils.logWarn('Could not parse screen dimensions, error details:', e); } } + var getEIDs = function(eids) { + if (utils.isArray(eids) && eids.length > 0) { + let src = []; + eids.forEach((eid) => { + let ids = []; + if (eid.uids) { + eid.uids.forEach(value => { + ids.push({'id': value.id + ''}); + }); + } + if (eid.source && ids.length > 0) { + src.push({source: eid.source, uids: ids}); + } + }); + return src; + } + } var state = {}; state['sid'] = siteId; state['l'] = topLocation(bidderRequest); @@ -131,6 +148,8 @@ function nobidBuildRequests(bids, bidderRequest) { if (sch) state['schain'] = sch; const cop = coppa(); if (cop) state['coppa'] = cop; + const eids = getEIDs(utils.deepAccess(bids, '0.userIdAsEids')); + if (eids && eids.length > 0) state['eids'] = eids; return state; } function newAdunit(adunitObject, adunits) { diff --git a/modules/oneVideoBidAdapter.js b/modules/oneVideoBidAdapter.js index c5bc054ac04..c287bc2f3b7 100644 --- a/modules/oneVideoBidAdapter.js +++ b/modules/oneVideoBidAdapter.js @@ -302,6 +302,14 @@ function getRequestData(bid, consentData, bidRequest) { bidData.site.ref = 'https://verizonmedia.com'; bidData.tmax = 1000; } + if (bid.params.video.custom && Object.prototype.toString.call(bid.params.video.custom) === '[object Object]') { + bidData.imp[0].ext.custom = {}; + for (const key in bid.params.video.custom) { + if (typeof bid.params.video.custom[key] === 'string' || typeof bid.params.video.custom[key] === 'number') { + bidData.imp[0].ext.custom[key] = bid.params.video.custom[key]; + } + } + } return bidData; } diff --git a/modules/oneVideoBidAdapter.md b/modules/oneVideoBidAdapter.md index 72f251aac04..92958af9e83 100644 --- a/modules/oneVideoBidAdapter.md +++ b/modules/oneVideoBidAdapter.md @@ -40,6 +40,10 @@ Connects to Verizon Media's Video SSP (AKA ONE Video / Adap.tv) demand source to inventoryid: 123, minduration: 10, maxduration: 30, + custom: { + key1: "value1", + key2: 123345 + } }, site: { id: 1, diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index fd66c8ce69f..1a2df023b81 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -7,11 +7,12 @@ import find from 'core-js-pure/features/array/find.js'; import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -const storage = getStorageManager(); - const ENDPOINT = 'https://onetag-sys.com/prebid-request'; const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; const BIDDER_CODE = 'onetag'; +const GVLID = 241; + +const storage = getStorageManager(GVLID); /** * Determines whether or not the given bid request is valid. @@ -231,7 +232,7 @@ function getPageInfo() { timing: getTiming(), version: { prebid: '$prebid.version$', - adapter: '1.0.0' + adapter: '1.1.0' } }; } @@ -379,6 +380,7 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: isBidRequestValid, buildRequests: buildRequests, diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index b153d0bf8db..d90572d1093 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -369,6 +369,18 @@ function addWurl(auctionId, adId, wurl) { } } +function getPbsResponseData(bidderRequests, response, pbsName, pbjsName) { + const bidderValues = utils.deepAccess(response, `ext.${pbsName}`); + if (bidderValues) { + Object.keys(bidderValues).forEach(bidder => { + let biddersReq = find(bidderRequests, bidderReq => bidderReq.bidderCode === bidder); + if (biddersReq) { + biddersReq[pbjsName] = bidderValues[bidder]; + } + }); + } +} + /** * @param {string} auctionId * @param {string} adId generated value set to bidObject.adId by bidderFactory Bid() @@ -676,6 +688,9 @@ const OPEN_RTB_PROTOCOL = { interpretResponse(response, bidderRequests) { const bids = []; + [['errors', 'serverErrors'], ['responsetimemillis', 'serverResponseTimeMs']] + .forEach(info => getPbsResponseData(bidderRequests, response, info[0], info[1])) + if (response.seatbid) { // a seatbid object contains a `bid` array and a `seat` string response.seatbid.forEach(seatbid => { @@ -698,6 +713,8 @@ const OPEN_RTB_PROTOCOL = { bidObject.cpm = cpm; + // temporarily leaving attaching it to each bidResponse so no breaking change + // BUT: this is a flat map, so it should be only attached to bidderRequest, a the change above does let serverResponseTimeMs = utils.deepAccess(response, ['ext', 'responsetimemillis', seatbid.seat].join('.')); if (bidRequest && serverResponseTimeMs) { bidRequest.serverResponseTimeMs = serverResponseTimeMs; @@ -733,8 +750,8 @@ const OPEN_RTB_PROTOCOL = { if (utils.deepAccess(bid, 'ext.prebid.type') === VIDEO) { bidObject.mediaType = VIDEO; let sizes = bidRequest.sizes && bidRequest.sizes[0]; - bidObject.playerHeight = sizes[0]; - bidObject.playerWidth = sizes[1]; + bidObject.playerWidth = sizes[0]; + bidObject.playerHeight = sizes[1]; // try to get cache values from 'response.ext.prebid.cache.js' // else try 'bid.ext.prebid.targeting' as fallback diff --git a/modules/prebidmanagerAnalyticsAdapter.js b/modules/prebidmanagerAnalyticsAdapter.js index b98ca864cd5..994ce4989f5 100644 --- a/modules/prebidmanagerAnalyticsAdapter.js +++ b/modules/prebidmanagerAnalyticsAdapter.js @@ -83,7 +83,7 @@ function collectUtmTagData() { if (newUtm === false) { utmTags.forEach(function (utmKey) { let itemValue = localStorage.getItem(`pm_${utmKey}`); - if (itemValue.length !== 0) { + if (itemValue && itemValue.length !== 0) { pmUtmTags[utmKey] = itemValue; } }); @@ -99,6 +99,16 @@ function collectUtmTagData() { return pmUtmTags; } +function collectPageInfo() { + const pageInfo = { + domain: window.location.hostname, + } + if (document.referrer) { + pageInfo.referrerDomain = utils.parseUrl(document.referrer).hostname; + } + return pageInfo; +} + function flush() { if (!pmAnalyticsEnabled) { return; @@ -111,6 +121,7 @@ function flush() { bundleId: initOptions.bundleId, events: _eventQueue, utmTags: collectUtmTagData(), + pageInfo: collectPageInfo(), }; ajax( diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 1b865e05c0a..fd8a46b172f 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -55,7 +55,7 @@ export let _floorDataForAuction = {}; * @summary Simple function to round up to a certain decimal degree */ function roundUp(number, precision) { - return Math.ceil(parseFloat(number) * Math.pow(10, precision)) / Math.pow(10, precision); + return Math.ceil((parseFloat(number) * Math.pow(10, precision)).toFixed(1)) / Math.pow(10, precision); } let referrerHostname; @@ -98,7 +98,7 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let fieldValues = enumeratePossibleFieldValues(utils.deepAccess(floorData, 'schema.fields') || [], bidObject, responseObject); if (!fieldValues.length) return { matchingFloor: floorData.default }; - // look to see iof a request for this context was made already + // look to see if a request for this context was made already let matchingInput = fieldValues.map(field => field[0]).join('-'); // if we already have gotten the matching rule from this matching input then use it! No need to look again let previousMatch = utils.deepAccess(floorData, `matchingInputs.${matchingInput}`); @@ -109,10 +109,12 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let matchingRule = find(allPossibleMatches, hashValue => floorData.values.hasOwnProperty(hashValue)); let matchingData = { - matchingFloor: floorData.values[matchingRule] || floorData.default, + floorMin: floorData.floorMin || 0, + floorRuleValue: floorData.values[matchingRule] || floorData.default, matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters matchingRule }; + matchingData.matchingFloor = Math.max(matchingData.floorMin, matchingData.floorRuleValue); // save for later lookup if needed utils.deepSetValue(floorData, `matchingInputs.${matchingInput}`, {...matchingData}); return matchingData; @@ -287,11 +289,12 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { bid.floorData = { skipped: floorData.skipped, skipRate: floorData.skipRate, + floorMin: floorData.floorMin, modelVersion: utils.deepAccess(floorData, 'data.modelVersion'), location: utils.deepAccess(floorData, 'data.location', 'noData'), floorProvider: floorData.floorProvider, fetchStatus: _floorsConfig.fetchStatus - } + }; }); }); } @@ -336,6 +339,8 @@ export function createFloorsDataForAuction(adUnits, auctionId) { const isSkipped = Math.random() * 100 < parseFloat(auctionSkipRate); resolvedFloorsData.skipped = isSkipped; } + // copy FloorMin to floorData.data + if (resolvedFloorsData.hasOwnProperty('floorMin')) resolvedFloorsData.data.floorMin = resolvedFloorsData.floorMin; // add floorData to bids updateAdUnitsForAuction(adUnits, resolvedFloorsData, auctionId); return resolvedFloorsData; @@ -568,6 +573,7 @@ function addFieldOverrides(overrides) { */ export function handleSetFloorsConfig(config) { _floorsConfig = utils.pick(config, [ + 'floorMin', 'enabled', enabled => enabled !== false, // defaults to true 'auctionDelay', auctionDelay => auctionDelay || 0, 'floorProvider', floorProvider => utils.deepAccess(config, 'data.floorProvider', floorProvider), @@ -623,6 +629,7 @@ function addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm) { bid.floorData = { floorValue: floorInfo.matchingFloor, floorRule: floorInfo.matchingRule, + floorRuleValue: floorInfo.floorRuleValue, floorCurrency: floorData.data.currency, cpmAfterAdjustments: adjustedCpm, enforcements: {...floorData.enforcement}, diff --git a/modules/pubCommonIdSystem.js b/modules/pubCommonIdSystem.js index 51b4335fe60..cb0c07cefa8 100644 --- a/modules/pubCommonIdSystem.js +++ b/modules/pubCommonIdSystem.js @@ -20,8 +20,9 @@ const SHAREDID_URL = 'https://id.sharedid.org/id'; const SHAREDID_SUFFIX = '_sharedid'; const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; const SHAREDID_DEFAULT_STATE = false; +const GVLID = 887; -const storage = getStorageManager(null, 'pubCommonId'); +const storage = getStorageManager(GVLID, 'pubCommonId'); /** * Store sharedid in either cookie or local storage @@ -37,7 +38,7 @@ function storeData(config, value) { if (config.storage.type === COOKIE) { if (storage.cookiesAreEnabled()) { - storage.setCookie(key, value, expiresStr, 'LAX', COOKIE_DOMAIN); + storage.setCookie(key, value, expiresStr, 'LAX', pubCommonIdSubmodule.domainOverride()); } } else if (config.storage.type === LOCAL_STORAGE) { if (storage.hasLocalStorage()) { @@ -159,7 +160,11 @@ export const pubCommonIdSubmodule = { * @type {string} */ name: MODULE_NAME, - + /** + * Vendor id of prebid + * @type {Number} + */ + gvlid: GVLID, /** * Return a callback function that calls the pixelUrl with id as a query parameter * @param pixelUrl @@ -282,16 +287,19 @@ export const pubCommonIdSubmodule = { domainOverride: function () { const domainElements = document.domain.split('.'); const cookieName = `_gd${Date.now()}`; - for (let i = 0, topDomain; i < domainElements.length; i++) { + for (let i = 0, topDomain, testCookie; i < domainElements.length; i++) { const nextDomain = domainElements.slice(i).join('.'); // write test cookie storage.setCookie(cookieName, '1', undefined, undefined, nextDomain); // read test cookie to verify domain was valid - if (storage.getCookie(cookieName) === '1') { - // delete test cookie - storage.setCookie(cookieName, '', 'Thu, 01 Jan 1970 00:00:01 GMT', undefined, nextDomain); + testCookie = storage.getCookie(cookieName); + + // delete test cookie + storage.setCookie(cookieName, '', 'Thu, 01 Jan 1970 00:00:01 GMT', undefined, nextDomain); + + if (testCookie === '1') { // cookie was written successfully using test domain so the topDomain is updated topDomain = nextDomain; } else { @@ -302,6 +310,4 @@ export const pubCommonIdSubmodule = { } }; -const COOKIE_DOMAIN = pubCommonIdSubmodule.domainOverride(); - submodule('userId', pubCommonIdSubmodule); diff --git a/modules/pubgeniusBidAdapter.js b/modules/pubgeniusBidAdapter.js index 05f18f99a9a..5c750e66c25 100644 --- a/modules/pubgeniusBidAdapter.js +++ b/modules/pubgeniusBidAdapter.js @@ -1,7 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; -import { BANNER } from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { deepAccess, deepSetValue, @@ -12,15 +12,16 @@ import { isStr, logError, parseQueryStringParameters, + pick, } from '../src/utils.js'; -const BIDDER_VERSION = '1.0.0'; +const BIDDER_VERSION = '1.1.0'; const BASE_URL = 'https://ortb.adpearl.io'; export const spec = { code: 'pubgenius', - supportedMediaTypes: [ BANNER ], + supportedMediaTypes: [ BANNER, VIDEO ], isBidRequestValid(bid) { const adUnitId = bid.params.adUnitId; @@ -29,15 +30,20 @@ export const spec = { return false; } - const sizes = deepAccess(bid, 'mediaTypes.banner.sizes'); - return !!(sizes && sizes.length) && sizes.every(size => isArrayOfNums(size, 2)); + const { mediaTypes } = bid; + + if (mediaTypes.banner) { + return isValidBanner(mediaTypes.banner); + } + + return isValidVideo(mediaTypes.video, bid.params.video); }, buildRequests: function (bidRequests, bidderRequest) { const data = { id: bidderRequest.auctionId, imp: bidRequests.map(buildImp), - tmax: config.getConfig('bidderTimeout'), + tmax: bidderRequest.timeout, ext: { pbadapter: { version: BIDDER_VERSION, @@ -141,16 +147,44 @@ export const spec = { }, }; +function buildVideoParams(videoMediaType, videoParams) { + videoMediaType = videoMediaType || {}; + const params = pick(videoMediaType, ['api', 'mimes', 'protocols', 'playbackmethod']); + + switch (videoMediaType.context) { + case 'instream': + params.placement = 1; + break; + case 'outstream': + params.placement = 2; + break; + default: + break; + } + + if (videoMediaType.playerSize) { + params.w = videoMediaType.playerSize[0][0]; + params.h = videoMediaType.playerSize[0][1]; + } + + return Object.assign(params, videoParams); +} + function buildImp(bid) { const imp = { id: bid.bidId, - banner: { - format: deepAccess(bid, 'mediaTypes.banner.sizes').map(size => ({ w: size[0], h: size[1] })), - topframe: numericBoolean(!inIframe()), - }, tagid: String(bid.params.adUnitId), }; + if (bid.mediaTypes.banner) { + imp.banner = { + format: bid.mediaTypes.banner.sizes.map(size => ({ w: size[0], h: size[1] })), + topframe: numericBoolean(!inIframe()), + }; + } else { + imp.video = buildVideoParams(bid.mediaTypes.video, bid.params.video); + } + const bidFloor = bid.params.bidFloor; if (isNumber(bidFloor)) { imp.bidfloor = bidFloor; @@ -197,7 +231,6 @@ function interpretBid(bid) { cpm: bid.price, width: bid.w, height: bid.h, - ad: bid.adm, ttl: bid.exp, creativeId: bid.crid, netRevenue: true, @@ -209,6 +242,24 @@ function interpretBid(bid) { }; } + const pbadapter = deepAccess(bid, 'ext.pbadapter') || {}; + switch (pbadapter.mediaType) { + case 'video': + if (bid.nurl) { + bidResponse.vastUrl = bid.nurl; + } + + if (bid.adm) { + bidResponse.vastXml = bid.adm; + } + + bidResponse.mediaType = VIDEO; + break; + default: // banner by default + bidResponse.ad = bid.adm; + break; + } + return bidResponse; } @@ -221,4 +272,22 @@ function getBaseUrl() { return (pubg && pubg.endpoint) || BASE_URL; } +function isValidSize(size) { + return isArrayOfNums(size, 2) && size[0] > 0 && size[1] > 0; +} + +function isValidBanner(banner) { + const sizes = banner.sizes; + return !!(sizes && sizes.length) && sizes.every(isValidSize); +} + +function isValidVideo(videoMediaType, videoParams) { + const params = buildVideoParams(videoMediaType, videoParams); + + return !!(params.placement && + isValidSize([params.w, params.h]) && + params.mimes && params.mimes.length && + isArrayOfNums(params.protocols) && params.protocols.length); +} + registerBidder(spec); diff --git a/modules/pubgeniusBidAdapter.md b/modules/pubgeniusBidAdapter.md index 66851af9c3f..ff23a433331 100644 --- a/modules/pubgeniusBidAdapter.md +++ b/modules/pubgeniusBidAdapter.md @@ -50,7 +50,40 @@ var adUnits = [ } } ] - } + }, + { + code: 'test-video', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 360], + mimes: ['video/mp4'], + protocols: [3], + } + }, + bids: [ + { + bidder: 'pubgenius', + params: { + adUnitId: '1001', + bidFloor: 1, + test: true, + + // other video parameters as in OpenRTB v2.5 spec + video: { + skip: 1 + + // the following overrides mediaTypes.video of the ad unit + placement: 1, + w: 640, + h: 360, + mimes: ['video/mp4'], + protocols: [3], + } + } + } + ] + }, ]; ``` diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js new file mode 100644 index 00000000000..7e2f5061621 --- /dev/null +++ b/modules/pubxaiAnalyticsAdapter.js @@ -0,0 +1,168 @@ +import { ajax } from '../src/ajax.js'; +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; +import * as utils from '../src/utils.js'; + +const emptyUrl = ''; +const analyticsType = 'endpoint'; +const pubxaiAnalyticsVersion = 'v1.0.0'; +const defaultHost = 'api.pbxai.com'; +const auctionPath = '/analytics/auction'; +const winningBidPath = '/analytics/bidwon'; + +let initOptions; +let auctionTimestamp; +let events = { + bids: [] +}; + +var pubxaiAnalyticsAdapter = Object.assign(adapter( + { + emptyUrl, + analyticsType + }), { + track({ eventType, args }) { + if (typeof args !== 'undefined') { + if (eventType === CONSTANTS.EVENTS.BID_TIMEOUT) { + args.forEach(item => { mapBidResponse(item, 'timeout'); }); + } else if (eventType === CONSTANTS.EVENTS.AUCTION_INIT) { + events.auctionInit = args; + auctionTimestamp = args.timestamp; + } else if (eventType === CONSTANTS.EVENTS.BID_REQUESTED) { + mapBidRequests(args).forEach(item => { events.bids.push(item) }); + } else if (eventType === CONSTANTS.EVENTS.BID_RESPONSE) { + mapBidResponse(args, 'response'); + } else if (eventType === CONSTANTS.EVENTS.BID_WON) { + send({ + winningBid: mapBidResponse(args, 'bidwon') + }, 'bidwon'); + } + } + + if (eventType === CONSTANTS.EVENTS.AUCTION_END) { + send(events, 'auctionEnd'); + } + } +}); + +function mapBidRequests(params) { + let arr = []; + if (typeof params.bids !== 'undefined' && params.bids.length) { + params.bids.forEach(function (bid) { + arr.push({ + bidderCode: bid.bidder, + bidId: bid.bidId, + adUnitCode: bid.adUnitCode, + requestId: bid.bidderRequestId, + auctionId: bid.auctionId, + transactionId: bid.transactionId, + sizes: utils.parseSizesInput(bid.mediaTypes.banner.sizes).toString(), + renderStatus: 1, + requestTimestamp: params.auctionStart + }); + }); + } + return arr; +} + +function mapBidResponse(bidResponse, status) { + if (status !== 'bidwon') { + let bid = events.bids.filter(o => o.bidId === bidResponse.bidId || o.bidId === bidResponse.requestId)[0]; + Object.assign(bid, { + bidderCode: bidResponse.bidder, + bidId: status === 'timeout' ? bidResponse.bidId : bidResponse.requestId, + adUnitCode: bidResponse.adUnitCode, + auctionId: bidResponse.auctionId, + creativeId: bidResponse.creativeId, + transactionId: bidResponse.transactionId, + currency: bidResponse.currency, + cpm: bidResponse.cpm, + netRevenue: bidResponse.netRevenue, + mediaType: bidResponse.mediaType, + statusMessage: bidResponse.statusMessage, + floorData: bidResponse.floorData, + status: bidResponse.status, + renderStatus: status === 'timeout' ? 3 : 2, + timeToRespond: bidResponse.timeToRespond, + requestTimestamp: bidResponse.requestTimestamp, + responseTimestamp: bidResponse.responseTimestamp, + platform: navigator.platform, + deviceType: getDeviceType() + }); + } else { + return { + bidderCode: bidResponse.bidder, + bidId: bidResponse.requestId, + adUnitCode: bidResponse.adUnitCode, + auctionId: bidResponse.auctionId, + creativeId: bidResponse.creativeId, + transactionId: bidResponse.transactionId, + currency: bidResponse.currency, + cpm: bidResponse.cpm, + netRevenue: bidResponse.netRevenue, + floorData: bidResponse.floorData, + renderedSize: bidResponse.size, + mediaType: bidResponse.mediaType, + statusMessage: bidResponse.statusMessage, + status: bidResponse.status, + renderStatus: 4, + timeToRespond: bidResponse.timeToRespond, + requestTimestamp: bidResponse.requestTimestamp, + responseTimestamp: bidResponse.responseTimestamp, + platform: navigator.platform, + deviceType: getDeviceType() + } + } +} + +export function getDeviceType() { + if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { + return 'tablet'; + } + if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { + return 'mobile'; + } + return 'desktop'; +} + +// add sampling rate +pubxaiAnalyticsAdapter.shouldFireEventRequest = function (samplingRate = 1) { + return (Math.floor((Math.random() * samplingRate + 1)) === parseInt(samplingRate)); +} + +function send(data, status) { + if (pubxaiAnalyticsAdapter.shouldFireEventRequest(initOptions.samplingRate)) { + let location = utils.getWindowLocation(); + if (typeof data !== 'undefined' && typeof data.auctionInit !== 'undefined') { + Object.assign(data.auctionInit, { host: location.host, path: location.pathname, search: location.search }); + } + data.initOptions = initOptions; + + let pubxaiAnalyticsRequestUrl = utils.buildUrl({ + protocol: 'https', + hostname: (initOptions && initOptions.hostName) || defaultHost, + pathname: status == 'bidwon' ? winningBidPath : auctionPath, + search: { + auctionTimestamp: auctionTimestamp, + pubxaiAnalyticsVersion: pubxaiAnalyticsVersion, + prebidVersion: $$PREBID_GLOBAL$$.version + } + }); + + ajax(pubxaiAnalyticsRequestUrl, undefined, JSON.stringify(data), { method: 'POST', contentType: 'text/plain' }); + } +} + +pubxaiAnalyticsAdapter.originEnableAnalytics = pubxaiAnalyticsAdapter.enableAnalytics; +pubxaiAnalyticsAdapter.enableAnalytics = function (config) { + initOptions = config.options; + pubxaiAnalyticsAdapter.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: pubxaiAnalyticsAdapter, + code: 'pubxai' +}); + +export default pubxaiAnalyticsAdapter; diff --git a/modules/pubxaiAnalyticsAdapter.md b/modules/pubxaiAnalyticsAdapter.md new file mode 100644 index 00000000000..112329fc171 --- /dev/null +++ b/modules/pubxaiAnalyticsAdapter.md @@ -0,0 +1,27 @@ +# Overview +Module Name: PubX.io Analytics Adapter +Module Type: Analytics Adapter +Maintainer: phaneendra@pubx.ai + +# Description + +Analytics adapter for prebid provided by Pubx.ai. Contact alex@pubx.ai for information. + +# Test Parameters + +``` +{ + provider: 'pubxai', + options : { + pubxId: 'xxx', + hostName: 'example.com', + samplingRate: 1 + } +} +``` +Property | Data Type | Is required? | Description |Example +:-----:|:-----:|:-----:|:-----:|:-----: +pubxId|string|Yes | A unique identifier provided by PubX.ai to indetify publishers. |`"a9d48e2f-24ec-4ec1-b3e2-04e32c3aeb03"` +hostName|string|No|hostName is provided by Pubx.ai. |`"https://example.com"` +samplingRate |number |No|How often the sampling must be taken. |`2` - (sample one in two cases) \ `3` - (sample one in three cases) + | | | | \ No newline at end of file diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index 12937dbec9d..f74d79a3dc5 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -121,10 +121,7 @@ function bidResponseAvailable(request, response) { netRevenue: DEFAULT_NET_REVENUE, currency: bidResponse.cur || DEFAULT_CURRENCY }; - if (idToImpMap[id]['native']) { - bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); - bid.mediaType = 'native'; - } else if (idToImpMap[id].video) { + if (idToImpMap[id].video) { // for outstream, a renderer is specified if (idToSlotConfig[id] && utils.deepAccess(idToSlotConfig[id], 'mediaTypes.video.context') === 'outstream') { bid.renderer = outstreamRenderer(utils.deepAccess(idToSlotConfig[id], 'renderer.options'), utils.deepAccess(idToBidMap[id], 'ext.outstream')); @@ -133,10 +130,13 @@ function bidResponseAvailable(request, response) { bid.mediaType = 'video'; bid.width = idToBidMap[id].w; bid.height = idToBidMap[id].h; - } else { + } else if (idToImpMap[id].banner) { bid.ad = idToBidMap[id].adm; bid.width = idToBidMap[id].w || idToImpMap[id].banner.w; bid.height = idToBidMap[id].h || idToImpMap[id].banner.h; + } else if (idToImpMap[id]['native']) { + bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); + bid.mediaType = 'native'; } bids.push(bid); } @@ -165,12 +165,12 @@ function impression(slot) { function banner(slot) { const sizes = parseSizes(slot); const size = adSize(slot, sizes); - return (slot.nativeParams || slot.params.video) ? null : { + return (slot.mediaTypes && slot.mediaTypes.banner) ? { w: size[0], h: size[1], battr: slot.params.battr, format: sizes - }; + } : null; } /** diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index 894bb991a71..e9541edb534 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -1,6 +1,7 @@ import * as utils from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; +import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import find from 'core-js-pure/features/array/find.js'; @@ -18,6 +19,9 @@ export const QUANTCAST_TEST_PUBLISHER = 'test-publisher'; export const QUANTCAST_TTL = 4; export const QUANTCAST_PROTOCOL = 'https'; export const QUANTCAST_PORT = '8443'; +export const QUANTCAST_FPA = '__qca'; + +export const storage = getStorageManager(QUANTCAST_VENDOR_ID, BIDDER_CODE); function makeVideoImp(bid) { const video = {}; @@ -101,15 +105,21 @@ function checkTCFv2(tcData) { return !!(vendorConsent && purposeConsent); } +function getQuantcastFPA() { + let fpa = storage.getCookie(QUANTCAST_FPA) + return fpa || '' +} + +let hasUserSynced = false; + /** * The documentation for Prebid.js Adapter 1.0 can be found at link below, * http://prebid.org/dev-docs/bidder-adapter-1.html */ export const spec = { code: BIDDER_CODE, - GVLID: 11, + GVLID: QUANTCAST_VENDOR_ID, supportedMediaTypes: ['banner', 'video'], - hasUserSynced: false, /** * Verify the `AdUnits.bids` response with `true` for valid request and `false` @@ -188,7 +198,8 @@ export const spec = { uspSignal: uspConsent ? 1 : 0, uspConsent, coppa: config.getConfig('coppa') === true ? 1 : 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: getQuantcastFPA() }; const data = JSON.stringify(requestData); @@ -271,7 +282,7 @@ export const spec = { }, getUserSyncs(syncOptions, serverResponses) { const syncs = [] - if (!this.hasUserSynced && syncOptions.pixelEnabled) { + if (!hasUserSynced && syncOptions.pixelEnabled) { const responseWithUrl = find(serverResponses, serverResponse => utils.deepAccess(serverResponse.body, 'userSync.url') ); @@ -283,12 +294,12 @@ export const spec = { url: url }); } - this.hasUserSynced = true; + hasUserSynced = true; } return syncs; }, resetUserSync() { - this.hasUserSynced = false; + hasUserSynced = false; } }; diff --git a/modules/qwarryBidAdapter.js b/modules/qwarryBidAdapter.js index 36c1562324a..7c2ec0f085b 100644 --- a/modules/qwarryBidAdapter.js +++ b/modules/qwarryBidAdapter.js @@ -19,7 +19,8 @@ export const spec = { validBidRequests.forEach(bidRequest => { bids.push({ bidId: bidRequest.bidId, - zoneToken: bidRequest.params.zoneToken + zoneToken: bidRequest.params.zoneToken, + pos: bidRequest.params.pos }) }) @@ -65,7 +66,9 @@ export const spec = { onBidWon: function (bid) { if (bid.winUrl) { - ajax(bid.winUrl, null); + const cpm = bid.cpm; + const winUrl = bid.winUrl.replace(/\$\{AUCTION_PRICE\}/, cpm); + ajax(winUrl, null); return true; } return false; diff --git a/modules/reconciliationRtdProvider.js b/modules/reconciliationRtdProvider.js new file mode 100644 index 00000000000..bd3d8861c5b --- /dev/null +++ b/modules/reconciliationRtdProvider.js @@ -0,0 +1,295 @@ +/** + * This module adds reconciliation provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will add custom targetings to ad units + * The module will listen to post messages from rendered creatives with Reconciliation Tag + * The module will call tracking pixels to log info needed for reconciliation matching + * @module modules/reconciliationRtdProvider + * @requires module:modules/realTimeData + */ + +/** + * @typedef {Object} ModuleParams + * @property {string} publisherMemberId + * @property {?string} initUrl + * @property {?string} impressionUrl + * @property {?boolean} allowAccess + */ + +import { submodule } from '../src/hook.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import * as utils from '../src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; + +/** @type {Object} */ +const MessageType = { + IMPRESSION_REQUEST: 'rsdk:impression:req', + IMPRESSION_RESPONSE: 'rsdk:impression:res', +}; +/** @type {ModuleParams} */ +const DEFAULT_PARAMS = { + initUrl: 'https://confirm.fiduciadlt.com/init', + impressionUrl: 'https://confirm.fiduciadlt.com/pimp', + allowAccess: false, +}; +/** @type {ModuleParams} */ +let _moduleParams = {}; + +/** + * Handle postMesssage from ad creative, track impression + * and send response to reconciliation ad tag + * @param {Event} e + */ +function handleAdMessage(e) { + let data = {}; + let adUnitId = ''; + let adDeliveryId = ''; + + try { + data = JSON.parse(e.data); + } catch (e) { + return; + } + + if (data.type === MessageType.IMPRESSION_REQUEST) { + if (utils.isGptPubadsDefined()) { + // 1. Find the last iframed window before window.top where the tracker was injected + // (the tracker could be injected in nested iframes) + const adWin = getTopIFrameWin(e.source); + if (adWin && adWin !== window.top) { + // 2. Find the GPT slot for the iframed window + const adSlot = getSlotByWin(adWin); + // 3. Get AdUnit IDs for the selected slot + if (adSlot) { + adUnitId = adSlot.getAdUnitPath(); + adDeliveryId = adSlot.getTargeting('RSDK_ADID'); + adDeliveryId = adDeliveryId.length + ? adDeliveryId[0] + : utils.generateUUID(); + } + } + } + + // Call local impression callback + const args = Object.assign({}, data.args, { + publisherDomain: window.location.hostname, + publisherMemberId: _moduleParams.publisherMemberId, + adUnitId, + adDeliveryId, + }); + + track.trackPost(_moduleParams.impressionUrl, args); + + // Send response back to the Advertiser tag + let response = { + type: MessageType.IMPRESSION_RESPONSE, + id: data.id, + args: Object.assign( + { + publisherDomain: window.location.hostname, + }, + data.args + ), + }; + + // If access is allowed - add ad unit id to response + if (_moduleParams.allowAccess) { + Object.assign(response.args, { + adUnitId, + adDeliveryId, + }); + } + + e.source.postMessage(JSON.stringify(response), '*'); + } +} + +/** + * Get top iframe window for nested Window object + * - top + * -- iframe.window <-- top iframe window + * --- iframe.window + * ---- iframe.window <-- win + * + * @param {Window} win nested iframe window object + * @param {Window} topWin top window + */ +export function getTopIFrameWin(win, topWin) { + topWin = topWin || window; + + if (!win) { + return null; + } + + try { + while (win.parent !== topWin) { + win = win.parent; + } + return win; + } catch (e) { + return null; + } +} + +/** + * get all slots on page + * @return {Object[]} slot GoogleTag slots + */ +function getAllSlots() { + return utils.isGptPubadsDefined() && window.googletag.pubads().getSlots(); +} + +/** + * get GPT slot by placement id + * @param {string} code placement id + * @return {?Object} + */ +function getSlotByCode(code) { + const slots = getAllSlots(); + if (!slots || !slots.length) { + return null; + } + return ( + find( + slots, + (s) => s.getSlotElementId() === code || s.getAdUnitPath() === code + ) || null + ); +} + +/** + * get GPT slot by iframe window + * @param {Window} win + * @return {?Object} + */ +export function getSlotByWin(win) { + const slots = getAllSlots(); + + if (!slots || !slots.length) { + return null; + } + + return ( + find(slots, (s) => { + let slotElement = document.getElementById(s.getSlotElementId()); + + if (slotElement) { + let slotIframe = slotElement.querySelector('iframe'); + + if (slotIframe && slotIframe.contentWindow === win) { + return true; + } + } + + return false; + }) || null + ); +} + +/** + * Init Reconciliation post messages listeners to handle + * impressions messages from ad creative + */ +function initListeners() { + window.addEventListener('message', handleAdMessage, false); +} + +/** + * Send init event to log + * @param {Array} adUnits + */ +function trackInit(adUnits) { + track.trackPost( + _moduleParams.initUrl, + { + adUnits, + publisherDomain: window.location.hostname, + publisherMemberId: _moduleParams.publisherMemberId, + } + ); +} + +/** + * Track event via POST request + * wrap method to allow stubbing in tests + * @param {string} url + * @param {Object} data + */ +export const track = { + trackPost(url, data) { + const ajax = ajaxBuilder(); + + ajax( + url, + function() {}, + JSON.stringify(data), + { + method: 'POST', + } + ); + } +} + +/** + * Set custom targetings for provided adUnits + * @param {string[]} adUnitsCodes + * @return {Object} key-value object with custom targetings + */ +function getReconciliationData(adUnitsCodes) { + const dataToReturn = {}; + const adUnitsToTrack = []; + + adUnitsCodes.forEach((adUnitCode) => { + if (!adUnitCode) { + return; + } + + const adSlot = getSlotByCode(adUnitCode); + const adUnitId = adSlot ? adSlot.getAdUnitPath() : adUnitCode; + const adDeliveryId = utils.generateUUID(); + + dataToReturn[adUnitCode] = { + RSDK_AUID: adUnitId, + RSDK_ADID: adDeliveryId, + }; + + adUnitsToTrack.push({ + adUnitId, + adDeliveryId + }); + }, {}); + + // Track init event + trackInit(adUnitsToTrack); + + return dataToReturn; +} + +/** @type {RtdSubmodule} */ +export const reconciliationSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: 'reconciliation', + /** + * get data and send back to realTimeData module + * @function + * @param {string[]} adUnitsCodes + */ + getTargetingData: getReconciliationData, + init: init, +}; + +function init(moduleConfig) { + const params = moduleConfig.params; + if (params && params.publisherMemberId) { + _moduleParams = Object.assign({}, DEFAULT_PARAMS, params); + initListeners(); + } else { + utils.logError('missing params for Reconciliation provider'); + } + return true; +} + +submodule('realTimeData', reconciliationSubmodule); diff --git a/modules/reconciliationRtdProvider.md b/modules/reconciliationRtdProvider.md new file mode 100644 index 00000000000..53883ad99eb --- /dev/null +++ b/modules/reconciliationRtdProvider.md @@ -0,0 +1,49 @@ +The purpose of this Real Time Data Provider is to allow publishers to match impressions accross the supply chain. + +**Reconciliation SDK** +The purpose of Reconciliation SDK module is to collect supply chain structure information and vendor-specific impression IDs from suppliers participating in ad creative delivery and report it to the Reconciliation Service, allowing publishers, advertisers and other supply chain participants to match and reconcile ad server, SSP, DSP and veritifation system log file records. Reconciliation SDK was created as part of TAG DLT initiative ( https://www.tagtoday.net/pressreleases/dlt_9_7_2020 ). + +**Usage for Publishers:** + +Compile the Reconciliation Provider into your Prebid build: + +`gulp build --modules=reconciliationRtdProvider` + +Add Reconciliation real time data provider configuration by setting up a Prebid Config: + +```javascript +const reconciliationDataProvider = { + name: "reconciliation", + params: { + publisherMemberId: "test_prebid_publisher", // required + allowAccess: true, //optional + } +}; + +pbjs.setConfig({ + ..., + realTimeData: { + dataProviders: [ + reconciliationDataProvider + ] + } +}); +``` + +where: +- `publisherMemberId` (required) - ID associated with the publisher +- `access` (optional) true/false - Whether ad markup will recieve Ad Unit Id's via Reconciliation Tag + +**Example:** + +To view an example: + +- in your cli run: + +`gulp serve --modules=reconciliationRtdProvider,appnexusBidAdapter` + +Your could also change 'appnexusBidAdapter' to another one. + +- in your browser, navigate to: + +`http://localhost:9999/integrationExamples/gpt/reconciliationRtdProvider_example.html` diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js index bc2854de40b..c77afbe6ec5 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -6,17 +6,13 @@ import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'relaido'; const BIDDER_DOMAIN = 'api.relaido.jp'; -const ADAPTER_VERSION = '1.0.1'; +const ADAPTER_VERSION = '1.0.2'; const DEFAULT_TTL = 300; const UUID_KEY = 'relaido_uuid'; const storage = getStorageManager(); function isBidRequestValid(bid) { - if (!utils.isSafariBrowser() && !hasUuid()) { - utils.logWarn('uuid is not found.'); - return false; - } if (!utils.deepAccess(bid, 'params.placementId')) { utils.logWarn('placementId param is reqeuired.'); return false; @@ -64,7 +60,7 @@ function buildRequests(validBidRequests, bidderRequest) { }; if (hasVideoMediaType(bidRequest)) { - const playerSize = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + const playerSize = getValidSizes(utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize')); payload.width = playerSize[0][0]; payload.height = playerSize[0][1]; } else if (hasBannerMediaType(bidRequest)) { @@ -101,10 +97,6 @@ function interpretResponse(serverResponse, bidRequest) { return []; } - if (body.uuid) { - storage.setDataInLocalStorage(UUID_KEY, body.uuid); - } - const playerUrl = bidRequest.player || body.playerUrl; const mediaType = bidRequest.mediaType || VIDEO; @@ -141,7 +133,6 @@ function getUserSyncs(syncOptions, serverResponses) { if (serverResponses.length > 0) { syncUrl = utils.deepAccess(serverResponses, '0.body.syncUrl') || syncUrl; } - receiveMessage(); return [{ type: 'iframe', url: syncUrl @@ -219,17 +210,6 @@ function outstreamRender(bid) { }); } -function receiveMessage() { - window.addEventListener('message', setUuid); -} - -function setUuid(e) { - if (utils.isPlainObject(e.data) && e.data.relaido_uuid) { - storage.setDataInLocalStorage(UUID_KEY, e.data.relaido_uuid); - window.removeEventListener('message', setUuid); - } -} - function isBannerValid(bid) { if (!isMobile()) { return false; @@ -242,8 +222,8 @@ function isBannerValid(bid) { } function isVideoValid(bid) { - const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); - if (playerSize && utils.isArray(playerSize) && playerSize.length > 0) { + const playerSize = getValidSizes(utils.deepAccess(bid, 'mediaTypes.video.playerSize')); + if (playerSize.length > 0) { const context = utils.deepAccess(bid, 'mediaTypes.video.context'); if (context && context === 'outstream') { return true; @@ -252,12 +232,12 @@ function isVideoValid(bid) { return false; } -function hasUuid() { - return !!storage.getDataFromLocalStorage(UUID_KEY); -} - function getUuid() { - return storage.getDataFromLocalStorage(UUID_KEY) || ''; + const id = storage.getCookie(UUID_KEY) + if (id) return id; + const newId = utils.generateUUID(); + storage.setCookie(UUID_KEY, newId); + return newId; } export function isMobile() { diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 3b899e2179d..a6b4202fc91 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -9,6 +9,7 @@ let REFERER = ''; export const spec = { code: BIDDER_CODE, + gvlid: 108, aliases: ['ra'], supportedMediaTypes: [BANNER, VIDEO], diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 9acd484cec8..e235868f791 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -3,16 +3,49 @@ * @module modules/realTimeData */ +/** + * @interface UserConsentData + */ +/** + * @property + * @summary gdpr consent + * @name UserConsentData#gdpr + * @type {Object} + */ +/** + * @property + * @summary usp consent + * @name UserConsentData#usp + * @type {Object} + */ +/** + * @property + * @summary coppa + * @name UserConsentData#coppa + * @type {boolean} + */ + /** * @interface RtdSubmodule */ /** - * @function + * @function? * @summary return real time data - * @name RtdSubmodule#getData - * @param {AdUnit[]} adUnits - * @param {function} onDone + * @name RtdSubmodule#getTargetingData + * @param {string[]} adUnitsCodes + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + +/** + * @function? + * @summary modify bid request data + * @name RtdSubmodule#getBidRequestData + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + * @param {Object} reqBidsConfigObj + * @param {function} callback */ /** @@ -33,42 +66,36 @@ * @function * @summary init sub module * @name RtdSubmodule#init - * @param {Object} config - * @param {Object} gdpr settings - * @param {Object} usp settings + * @param {SubmoduleConfig} config + * @param {UserConsentData} user consent * @return {boolean} false to remove sub module */ /** * @function? * @summary on auction init event - * @name RtdSubmodule#auctionInit + * @name RtdSubmodule#onAuctionInitEvent * @param {Object} data * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent */ /** * @function? * @summary on auction end event - * @name RtdSubmodule#auctionEnd - * @param {Object} data - * @param {SubmoduleConfig} config - */ - -/** - * @function? - * @summary on bid request event - * @name RtdSubmodule#updateBidRequest + * @name RtdSubmodule#onAuctionEndEvent * @param {Object} data * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent */ /** * @function? * @summary on bid response event - * @name RtdSubmodule#updateBidResponse + * @name RtdSubmodule#onBidResponseEvent * @param {Object} data * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent */ /** @@ -82,13 +109,6 @@ * @type {number} */ -/** - * @property - * @summary timeout (if no auction dealy) - * @name ModuleConfig#timeout - * @type {number} - */ - /** * @property * @summary list of sub modules @@ -121,33 +141,34 @@ * @type {boolean} */ -import {getGlobal} from '../../src/prebidGlobal.js'; import {config} from '../../src/config.js'; -import {targeting} from '../../src/targeting.js'; -import {getHook, module} from '../../src/hook.js'; +import {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'; +import {getGlobal} from '../../src/prebidGlobal.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; -/** @type {number} */ -const DEF_TIMEOUT = 1000; +/** @type {RtdSubmodule[]} */ +let registeredSubModules = []; /** @type {RtdSubmodule[]} */ export let subModules = []; /** @type {ModuleConfig} */ let _moduleConfig; /** @type {SubmoduleConfig[]} */ let _dataProviders = []; +/** @type {UserConsentData} */ +let _userConsent; /** * enable submodule in User ID * @param {RtdSubmodule} submodule */ export function attachRealTimeDataProvider(submodule) { - subModules.push(submodule); + registeredSubModules.push(submodule); } export function init(config) { @@ -159,35 +180,35 @@ 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; - } - // delay bidding process only if auctionDelay > 0 - if (!_moduleConfig.auctionDelay > 0) { - getHook('bidsBackCallback').before(setTargetsAfterRequestBids); - } else { - getGlobal().requestBids.before(requestBidsHook); - } + getGlobal().requestBids.before(setBidRequestsData, 40); + initSubModules(); }); } +function getConsentData() { + return { + gdpr: gdprDataHandler.getConsentData(), + usp: uspDataHandler.getConsentData(), + coppa: !!(config.getConfig('coppa')) + } +} + /** * 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) { +function initSubModules() { + _userConsent = getConsentData(); 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()); + const sm = find(registeredSubModules, s => s.name === provider.name); + const initResponse = sm && sm.init && sm.init(provider, _userConsent); if (initResponse) { subModulesByOrder.push(Object.assign(sm, {config: provider})); } }); subModules = subModulesByOrder; - next(adUnits, auctionStart, auctionId, cbTimeout, labels) } /** @@ -195,94 +216,117 @@ export function initSubModules(next, adUnits, auctionStart, auctionId, cbTimeout */ function setEventsListeners() { events.on(CONSTANTS.EVENTS.AUCTION_INIT, (args) => { - subModules.forEach(sm => { sm.auctionInit && sm.auctionInit(args, sm.config) }) + subModules.forEach(sm => { sm.onAuctionInitEvent && sm.onAuctionInitEvent(args, sm.config, _userConsent) }) }); 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) }) + getAdUnitTargeting(args); + subModules.forEach(sm => { sm.onAuctionEndEvent && sm.onAuctionEndEvent(args, sm.config, _userConsent) }) }); events.on(CONSTANTS.EVENTS.BID_RESPONSE, (args) => { - subModules.forEach(sm => { sm.updateBidResponse && sm.updateBidResponse(args, sm.config) }) + subModules.forEach(sm => { sm.onBidResponseEvent && sm.onBidResponseEvent(args, sm.config, _userConsent) }) }); } /** - * get data from sub module - * @param {AdUnit[]} adUnits received from auction - * @param {function} callback callback function on data received + * loop through configured data providers If the data provider has registered getBidRequestData, + * call it, providing reqBidsConfigObj, consent data and module params + * this allows submodules to modify bidders + * @param {Object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. + * @param {function} fn required; The next function in the chain, used by hook.js */ -export function 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); +export function setBidRequestsData(fn, reqBidsConfigObj) { + _userConsent = getConsentData(); + + const relevantSubModules = []; + const prioritySubModules = []; subModules.forEach(sm => { - sm.getData(adUnits, onDataReceived.bind(sm)); + if (typeof sm.getBidRequestData !== 'function') { + return; + } + relevantSubModules.push(sm); + const config = sm.config; + if (config && config.waitForIt) { + prioritySubModules.push(sm); + } }); - function onDataReceived(data) { - if (processDone) { - return + const shouldDelayAuction = prioritySubModules.length && _moduleConfig.auctionDelay && _moduleConfig.auctionDelay > 0; + let callbacksExpected = prioritySubModules.length; + let isDone = false; + let waitTimeout; + + if (!relevantSubModules.length) { + return exitHook(); + } + + if (shouldDelayAuction) { + waitTimeout = setTimeout(exitHook, _moduleConfig.auctionDelay); + } + + relevantSubModules.forEach(sm => { + sm.getBidRequestData(reqBidsConfigObj, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent) + }); + + if (!shouldDelayAuction) { + return exitHook(); + } + + function onGetBidRequestDataCallback() { + if (isDone) { + return; } - dataReceived[this.name] = data; - if (shouldWaitForAllSubModules || (this.config && this.config.waitForIt)) { - callbacksExpected-- + if (this.config && this.config.waitForIt) { + callbacksExpected--; } if (callbacksExpected <= 0) { - clearTimeout(dataWaitTimeout); - done(); + return exitHook(); } } - function done() { - processDone = true; - callback(dataReceived); + function exitHook() { + isDone = true; + clearTimeout(waitTimeout); + fn.call(this, reqBidsConfigObj); } } /** - * run hook after bids request and before callback - * get data from provider and set key values to primary ad server - * @param {function} next - next hook function - * @param {AdUnit[]} adUnits received from auction + * loop through configured data providers If the data provider has registered getTargetingData, + * call it, providing ad unit codes, consent data and module params + * the sub mlodle will return data to set on the ad unit + * this function used to place key values on primary ad server per ad unit + * @param {Object} auction object received on auction end event */ -export function setTargetsAfterRequestBids(next, adUnits) { - getProviderData(adUnits, (data) => { - if (data && Object.keys(data).length) { - const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); - if (Object.keys(_mergedData).length) { - setDataForPrimaryAdServer(_mergedData); - } - } - next(adUnits); - }); -} +export function getAdUnitTargeting(auction) { + const relevantSubModules = subModules.filter(sm => typeof sm.getTargetingData === 'function'); + if (!relevantSubModules.length) { + return; + } -/** - * 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]) + // get data + const adUnitCodes = auction.adUnitCodes; + if (!adUnitCodes) { + return; + } + let targeting = []; + for (let i = relevantSubModules.length - 1; i >= 0; i--) { + const smTargeting = relevantSubModules[i].getTargetingData(adUnitCodes, relevantSubModules[i].config, _userConsent); + if (smTargeting && typeof smTargeting === 'object') { + targeting.push(smTargeting); + } else { + utils.logWarn('invalid getTargetingData response for sub module', relevantSubModules[i].name); } } - return rd; + // place data on auction adUnits + const mergedTargeting = deepMerge(targeting); + auction.adUnits.forEach(adUnit => { + const kv = adUnit.code && mergedTargeting[adUnit.code]; + if (!kv) { + return + } + adUnit[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] = Object.assign(adUnit[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] || {}, kv); + }); + return auction.adUnits; } /** @@ -311,53 +355,5 @@ export function deepMerge(arr) { }, {}); } -/** - * run hook before bids request - * get data from provider and set key values to primary ad server & bidders - * @param {function} fn - hook function - * @param {Object} reqBidsConfigObj - request bids object - */ -export function requestBidsHook(fn, reqBidsConfigObj) { - getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits, (data) => { - if (data && Object.keys(data).length) { - const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); - if (Object.keys(_mergedData).length) { - setDataForPrimaryAdServer(_mergedData); - addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, _mergedData); - } - } - return fn.call(this, reqBidsConfigObj); - }); -} - -/** - * set data to primary ad server - * @param {Object} data - key values to set - */ -function setDataForPrimaryAdServer(data) { - if (utils.isGptPubadsDefined()) { - targeting.setTargetingForGPT(data, null) - } else { - window.googletag = window.googletag || {}; - window.googletag.cmd = window.googletag.cmd || []; - window.googletag.cmd.push(() => { - targeting.setTargetingForGPT(data, null); - }); - } -} - -/** - * @param {AdUnit[]} adUnits - * @param {Object} data - key values to set - */ -function addIdDataToAdUnitBids(adUnits, data) { - adUnits.forEach(adUnit => { - adUnit.bids = adUnit.bids.map(bid => { - const rd = data[adUnit.code] || {}; - return Object.assign(bid, {realTimeData: rd}); - }) - }); -} - module('realTimeData', attachRealTimeDataProvider); init(config); diff --git a/modules/rtdModule/provider.md b/modules/rtdModule/provider.md index fb42e7188d3..116db160238 100644 --- a/modules/rtdModule/provider.md +++ b/modules/rtdModule/provider.md @@ -1,27 +1,33 @@ New provider must include the following: -1. sub module object: -``` -export const subModuleName = { - name: String, - getData: Function -}; -``` +1. sub module object with the following keys: -2. Function that returns the real time data according to the following structure: -``` +| param name | type | Scope | Description | Params | +| :------------ | :------------ | :------ | :------ | :------ | +| name | string | required | must match the name provided by the publisher in the on-page config | n/a | +| init | function | required | defines the function that does any auction-level initialization required | config, userConsent | +| getTargetingData | function | optional | defines a function that provides ad server targeting data to RTD-core | adUnitArray, config, userConsent | +| getBidRequestData | function | optional | defines a function that provides ad server targeting data to RTD-core | reqBidsConfigObj, callback, config, userConsent | +| onAuctionInitEvent | function | optional | listens to the AUCTION_INIT event and calls a sub-module function that lets it inspect and/or update the auction | auctionDetails, config, userConsent | +| onAuctionEndEvent | function |optional | listens to the AUCTION_END event and calls a sub-module function that lets it know when auction is done | auctionDetails, config, userConsent | +| onBidResponseEvent | function |optional | listens to the BID_RESPONSE event and calls a sub-module function that lets it know when a bid response has been collected | bidResponse, config, userConsent | + +2. `getTargetingData` function (if defined) should return ad unit targeting data according to the following structure: +```json { "adUnitCode":{ "key":"value", "key2":"value" }, "adUnitCode2":{ - "dataKey":"dataValue", + "dataKey":"dataValue" } } ``` 3. Hook to Real Time Data module: -``` +```javascript submodule('realTimeData', subModuleName); ``` + +4. See detailed documentation [here](https://docs.prebid.org/dev-docs/add-rtd-submodule.html) diff --git a/modules/rtdModule/realTimeData.md b/modules/rtdModule/realTimeData.md deleted file mode 100644 index b2859098b1f..00000000000 --- a/modules/rtdModule/realTimeData.md +++ /dev/null @@ -1,32 +0,0 @@ -## Real Time Data Configuration Example - -Example showing config using `browsi` sub module -``` - pbjs.setConfig({ - "realTimeData": { - "auctionDelay": 1000, - dataProviders[{ - "name": "browsi", - "params": { - "url": "testUrl.com", - "siteKey": "testKey", - "pubKey": "testPub", - "keyName":"bv" - } - }] - } - }); -``` - -Example showing real time data object received form `browsi` real time data provider -``` -{ - "adUnitCode":{ - "key":"value", - "key2":"value" - }, - "adUnitCode2":{ - "dataKey":"dataValue", - } -} -``` diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index f6d30e06e9a..ff8cb7895b9 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -13,6 +13,14 @@ const COOKIE_NAME = 'rpaSession'; const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins const END_EXPIRE_TIME = 21600000; // 6 hours +const pbsErrorMap = { + 1: 'timeout-error', + 2: 'input-error', + 3: 'connect-error', + 4: 'request-error', + 999: 'generic-error' +} + let prebidGlobal = getGlobal(); const { EVENTS: { @@ -117,7 +125,7 @@ function sendMessage(auctionId, bidWonId) { if (source) { return source; } - return serverConfig && Array.isArray(serverConfig.bidders) && serverConfig.bidders.indexOf(bid.bidder) !== -1 + return serverConfig && Array.isArray(serverConfig.bidders) && serverConfig.bidders.some(s2sBidder => s2sBidder.toLowerCase() === bid.bidder) !== -1 ? 'server' : 'client' }, 'clientLatencyMillis', @@ -129,6 +137,7 @@ function sendMessage(auctionId, bidWonId) { 'dimensions', 'mediaType', 'floorValue', + 'floorRuleValue', 'floorRule' ]) : undefined ]); @@ -233,6 +242,7 @@ function sendMessage(auctionId, bidWonId) { 'dealsEnforced', () => utils.deepAccess(auctionCache.floorData, 'enforcements.floorDeals'), 'skipRate', 'fetchStatus', + 'floorMin', 'floorProvider as provider' ]); } @@ -344,11 +354,34 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { }, 'seatBidId', 'floorValue', () => utils.deepAccess(bid, 'floorData.floorValue'), + 'floorRuleValue', () => utils.deepAccess(bid, 'floorData.floorRuleValue'), 'floorRule', () => utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined ]); } +/* + Filters and converts URL Params into an object and returns only KVs that match the 'utm_KEY' format +*/ +function getUtmParams() { + let search; + + try { + search = utils.parseQS(utils.getWindowLocation().search); + } catch (e) { + search = {}; + } + + return Object.keys(search).reduce((accum, param) => { + if (param.match(/utm_/)) { + accum[param.replace(/utm_/, '')] = search[param]; + } + return accum; + }, {}); +} + function getFpkvs() { + rubiConf.fpkvs = Object.assign((rubiConf.fpkvs || {}), getUtmParams()); + const isValid = rubiConf.fpkvs && typeof rubiConf.fpkvs === 'object' && Object.keys(rubiConf.fpkvs).every(key => typeof rubiConf.fpkvs[key] === 'string'); return isValid ? rubiConf.fpkvs : {}; } @@ -522,7 +555,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { 'bidder', bidder => bidder.toLowerCase(), 'bidId', 'status', () => 'no-bid', // default a bid to no-bid until response is recieved or bid is timed out - 'finalSource as source', + 'source', () => formatSource(bid.src), 'params', (params, bid) => { switch (bid.bidder) { // specify bidder params we want here @@ -628,10 +661,22 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { bid.bidResponse = parseBidResponse(args, bid.bidResponse); break; case BIDDER_DONE: + const serverError = utils.deepAccess(args, 'serverErrors.0'); + const serverResponseTimeMs = args.serverResponseTimeMs; args.bids.forEach(bid => { let cachedBid = cache.auctions[bid.auctionId].bids[bid.bidId || bid.requestId]; if (typeof bid.serverResponseTimeMs !== 'undefined') { cachedBid.serverLatencyMillis = bid.serverResponseTimeMs; + } else if (serverResponseTimeMs && bid.source === 's2s') { + cachedBid.serverLatencyMillis = serverResponseTimeMs; + } + // if PBS said we had an error, and this bid has not been processed by BID_RESPONSE YET + if (serverError && (!cachedBid.status || ['no-bid', 'error'].indexOf(cachedBid.status) !== -1)) { + cachedBid.status = 'error'; + cachedBid.error = { + code: pbsErrorMap[serverError.code] || pbsErrorMap[999], + description: serverError.message + } } if (!cachedBid.status) { cachedBid.status = 'no-bid'; @@ -672,10 +717,14 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { args.forEach(badBid => { let auctionCache = cache.auctions[badBid.auctionId]; let bid = auctionCache.bids[badBid.bidId || badBid.requestId]; - bid.status = 'error'; - bid.error = { - code: 'timeout-error' - }; + // might be set already by bidder-done, so do not overwrite + if (bid.status !== 'error') { + bid.status = 'error'; + bid.error = { + code: 'timeout-error', + message: 'marked by prebid.js as timeout' // will help us diff if timeout was set by PBS or PBJS + }; + } }); break; } diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 069a7e7ead5..e439f7fd2a4 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -238,15 +238,6 @@ export const spec = { const eids = utils.deepAccess(bidderRequest, 'bids.0.userIdAsEids'); if (eids && eids.length) { utils.deepSetValue(data, 'user.ext.eids', eids); - - // 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); - } - } } // set user.id value from config value @@ -374,6 +365,7 @@ export const spec = { getOrderedParams: function(params) { const containsTgV = /^tg_v/ const containsTgI = /^tg_i/ + const containsUId = /^eid_|^tpid_/ const orderedParams = [ 'account_id', @@ -386,18 +378,15 @@ export const spec = { 'gdpr_consent', 'us_privacy', 'rp_schain', - 'tpid_tdid', - 'tpid_liveintent.com', - 'tg_v.LIseg', - 'ppuid', - 'eid_pubcid.org', - 'eid_sharedid.org', - 'eid_criteo.com', - 'rf', - 'p_geo.latitude', - 'p_geo.longitude', - 'kw' - ].concat(Object.keys(params).filter(item => containsTgV.test(item))) + ].concat(Object.keys(params).filter(item => containsUId.test(item))) + .concat([ + 'x_liverampidl', + 'ppuid', + 'rf', + 'p_geo.latitude', + 'p_geo.longitude', + 'kw' + ]).concat(Object.keys(params).filter(item => containsTgV.test(item))) .concat(Object.keys(params).filter(item => containsTgI.test(item))) .concat([ 'tk_flint', @@ -471,7 +460,7 @@ export const spec = { 'zone_id': params.zoneId, 'size_id': parsedSizes[0], 'alt_size_ids': parsedSizes.slice(1).join(',') || undefined, - 'rp_floor': (params.floor = parseFloat(params.floor)) > 0.01 ? params.floor : 0.01, + 'rp_floor': (params.floor = parseFloat(params.floor)) >= 0.01 ? params.floor : undefined, 'rp_secure': '1', 'tk_flint': `${rubiConf.int_type || DEFAULT_INTEGRATION}_v$prebid.version$`, 'x_source.tid': bidRequest.transactionId, @@ -503,51 +492,47 @@ export const spec = { // For SRA we need to explicitly put empty semi colons so AE treats it as empty, instead of copying the latter value data['p_pos'] = (params.position === 'atf' || params.position === 'btf') ? params.position : ''; - if (bidRequest.userIdAsEids && bidRequest.userIdAsEids.length) { - const unifiedId = find(bidRequest.userIdAsEids, eid => eid.source === 'adserver.org'); - if (unifiedId) { - data['tpid_tdid'] = unifiedId.uids[0].id; - } - const liveintentId = find(bidRequest.userIdAsEids, eid => eid.source === 'liveintent.com'); - if (liveintentId) { - data['tpid_liveintent.com'] = liveintentId.uids[0].id; - if (liveintentId.ext && Array.isArray(liveintentId.ext.segments) && liveintentId.ext.segments.length) { - data['tg_v.LIseg'] = liveintentId.ext.segments.join(','); - } - } - const liverampId = find(bidRequest.userIdAsEids, eid => eid.source === 'liveramp.com'); - if (liverampId) { - data['x_liverampidl'] = liverampId.uids[0].id; - } - const sharedId = find(bidRequest.userIdAsEids, eid => eid.source === 'sharedid.org'); - if (sharedId) { - data['eid_sharedid.org'] = `${sharedId.uids[0].id}^${sharedId.uids[0].atype}^${sharedId.uids[0].ext.third}`; - } - const pubcid = find(bidRequest.userIdAsEids, eid => eid.source === 'pubcid.org'); - if (pubcid) { - data['eid_pubcid.org'] = `${pubcid.uids[0].id}^${pubcid.uids[0].atype}`; - } - const criteoId = find(bidRequest.userIdAsEids, eid => eid.source === 'criteo.com'); - if (criteoId) { - data['eid_criteo.com'] = `${criteoId.uids[0].id}^${criteoId.uids[0].atype}`; - } - } - - // set ppuid value from config value + // pass publisher provided userId if configured const configUserId = config.getConfig('user.id'); if (configUserId) { data['ppuid'] = configUserId; - } else { - // if config.getConfig('user.id') doesn't return anything, then look for the first eid.uids[*].ext.stype === 'ppuid' - for (let i = 0; bidRequest.userIdAsEids && i < bidRequest.userIdAsEids.length; i++) { - if (bidRequest.userIdAsEids[i].uids) { - const pubProvidedId = find(bidRequest.userIdAsEids[i].uids, uid => uid.ext && uid.ext.stype === 'ppuid'); - if (pubProvidedId && pubProvidedId.id) { - data['ppuid'] = pubProvidedId.id; - break; + } + // loop through userIds and add to request + if (bidRequest.userIdAsEids) { + bidRequest.userIdAsEids.forEach(eid => { + try { + // special cases + if (eid.source === 'adserver.org') { + data['tpid_tdid'] = eid.uids[0].id; + data['eid_adserver.org'] = eid.uids[0].id; + } else if (eid.source === 'liveintent.com') { + data['tpid_liveintent.com'] = eid.uids[0].id; + data['eid_liveintent.com'] = eid.uids[0].id; + if (eid.ext && Array.isArray(eid.ext.segments) && eid.ext.segments.length) { + data['tg_v.LIseg'] = eid.ext.segments.join(','); + } + } else if (eid.source === 'liveramp.com') { + data['x_liverampidl'] = eid.uids[0].id; + } else if (eid.source === 'sharedid.org') { + data['eid_sharedid.org'] = `${eid.uids[0].id}^${eid.uids[0].atype}^${(eid.uids[0].ext && eid.uids[0].ext.third) || ''}`; + } else if (eid.source === 'id5-sync.com') { + data['eid_id5-sync.com'] = `${eid.uids[0].id}^${eid.uids[0].atype}^${(eid.ext && eid.ext.linkType) || ''}`; + } else { + // add anything else with this generic format + data[`eid_${eid.source}`] = `${eid.uids[0].id}^${eid.uids[0].atype || ''}`; + } + // send AE "ppuid" signal if exists, and hasn't already been sent + if (!data['ppuid']) { + // get the first eid.uids[*].ext.stype === 'ppuid', if one exists + const ppId = find(eid.uids, uid => uid.ext && uid.ext.stype === 'ppuid'); + if (ppId && ppId.id) { + data['ppuid'] = ppId.id; + } } + } catch (e) { + utils.logWarn('Rubicon: error reading eid:', eid, e); } - } + }); } if (bidderRequest.gdprConsent) { @@ -685,6 +670,14 @@ export const spec = { bidObject.dealId = bid.dealid; } + if (bid.adomain) { + utils.deepSetValue(bidObject, 'meta.advertiserDomains', Array.isArray(bid.adomain) ? bid.adomain : [bid.adomain]); + } + + if (utils.deepAccess(bid, 'ext.bidder.rp.advid')) { + utils.deepSetValue(bidObject, 'meta.advertiserId', bid.ext.bidder.rp.advid); + } + let serverResponseTimeMs = utils.deepAccess(responseObj, 'ext.responsetimemillis.rubicon'); if (bidRequest && serverResponseTimeMs) { bidRequest.serverResponseTimeMs = serverResponseTimeMs; @@ -692,6 +685,7 @@ export const spec = { if (utils.deepAccess(bid, 'ext.prebid.type') === VIDEO) { bidObject.mediaType = VIDEO; + utils.deepSetValue(bidObject, 'meta.mediaType', VIDEO); const extPrebidTargeting = utils.deepAccess(bid, 'ext.prebid.targeting'); // If ext.prebid.targeting exists, add it as a property value named 'adserverTargeting' @@ -757,7 +751,7 @@ export const spec = { advertiserId: ad.advertiser, networkId: ad.network }, meta: { - advertiserId: ad.advertiser, networkId: ad.network + advertiserId: ad.advertiser, networkId: ad.network, mediaType: BANNER } }; @@ -765,6 +759,10 @@ export const spec = { bid.mediaType = ad.creative_type; } + if (ad.adomain) { + bid.meta.advertiserDomains = Array.isArray(ad.adomain) ? ad.adomain : [ad.adomain]; + } + if (ad.creative_type === VIDEO) { bid.width = associatedBidRequest.params.video.playerWidth; bid.height = associatedBidRequest.params.video.playerHeight; diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index cdd840c4f54..49cac46f1df 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -21,6 +21,7 @@ const TIME_MAX = Math.pow(2, 48) - 1; const TIME_LEN = 10; const RANDOM_LEN = 16; const id = factory(); +const GVLID = 887; /** * Constructs cookie value * @param value @@ -283,6 +284,11 @@ export const sharedIdSubmodule = { */ name: MODULE_NAME, + /** + * Vendor id of Prebid + * @type {Number} + */ + gvlid: GVLID, /** * decode the stored id value for passing to bid requests * @function diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 0d183be05df..7df161db713 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -50,6 +50,12 @@ export const sharethroughAdapterSpec = { query.ttduid = bidRequest.userId.tdid; } + if (bidRequest.userId && bidRequest.userId.pubcid) { + query.pubcid = bidRequest.userId.pubcid; + } else if (bidRequest.crumbs && bidRequest.crumbs.pubcid) { + query.pubcid = bidRequest.crumbs.pubcid; + } + if (bidRequest.schain) { query.schain = JSON.stringify(bidRequest.schain); } diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index 49b4ed6aa34..93915689cee 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -117,6 +117,11 @@ const buildOpenRtbBidRequestPayload = (validBidRequests, bidderRequest) => { utils.deepSetValue(request, 'device.ifa', ifa); } + const eids = utils.deepAccess(validBidRequests[0], 'userIdAsEids'); + if (eids && eids.length) { + utils.deepSetValue(request, 'user.ext.eids', eids); + } + utils.logInfo('[SMAATO] OpenRTB Request:', request); return JSON.stringify(request); } diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index 8462e749b91..ed9003e3b4d 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -13,8 +13,10 @@ import { createEidsArray } from './userId/eids.js'; const BIDDER_CODE = 'smartadserver'; +const GVL_ID = 45; export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, aliases: ['smart'], // short code supportedMediaTypes: [BANNER, VIDEO], /** @@ -99,6 +101,7 @@ export const spec = { } if (bidderRequest && bidderRequest.gdprConsent) { + payload.addtl_consent = bidderRequest.gdprConsent.addtlConsent; payload.gdpr_consent = bidderRequest.gdprConsent.consentString; payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side } diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 935b0ceb489..62f5e85779e 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -143,7 +143,7 @@ export const spec = { netRevenue: true, mediaType: BANNER, ad: decodeURIComponent(`${sovrnBid.adm}`), - ttl: 60 + ttl: sovrnBid.ttl || 90 }); }); } diff --git a/modules/sspBCAdapter.js b/modules/sspBCAdapter.js index ef89fb08449..4069c722e9d 100644 --- a/modules/sspBCAdapter.js +++ b/modules/sspBCAdapter.js @@ -1,14 +1,17 @@ import * as utils from '../src/utils.js'; +import { ajax } from '../src/ajax.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 NOTIFY_URL = 'https://ssp.wp.pl/bidder/notify'; const TMAX = 450; -const BIDDER_VERSION = '4.5'; +const BIDDER_VERSION = '4.6'; const W = window; const { navigator } = W; +var consentApiVersion; const cookieSupport = () => { const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); @@ -53,6 +56,7 @@ const applyClientHints = ortbRequest => { function applyGdpr(bidderRequest, ortbRequest) { if (bidderRequest && bidderRequest.gdprConsent) { + consentApiVersion = bidderRequest.gdprConsent.apiVersion; 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 }); } @@ -68,6 +72,14 @@ function setOnAny(collection, key) { } } +function sendNotification(payload) { + ajax(NOTIFY_URL, null, JSON.stringify(payload), { + withCredentials: false, + method: 'POST', + crossOrigin: true + }); +} + /** * @param {object} slot Ad Unit Params by Prebid * @returns {object} Banner by OpenRTB 2.5 §3.2.6 @@ -278,6 +290,7 @@ const spec = { mediaType: 'banner', meta: { advertiserDomains: serverBid.adomain, + networkName: seat, }, netRevenue: true, ad: renderCreative(site, response.id, serverBid, seat, request.bidderRequest), @@ -303,12 +316,44 @@ const spec = { if (syncOptions.iframeEnabled) { return [{ type: 'iframe', - url: SYNC_URL, + url: SYNC_URL + '?tcf=' + consentApiVersion, }]; } utils.logWarn('sspBC adapter requires iframe based user sync.'); }, - onTimeout() { + + onTimeout(timeoutData) { + var adSlots = []; + const bid = timeoutData && timeoutData[0]; + if (bid) { + timeoutData.forEach(bid => { adSlots.push(bid.params[0] && bid.params[0].id) }) + const payload = { + event: 'timeout', + requestId: bid.auctionId, + siteId: bid.params ? [bid.params[0].siteId] : [], + slotId: adSlots, + timeout: bid.timeout, + } + sendNotification(payload); + return payload; + } + }, + + onBidWon(bid) { + if (bid && bid.auctionId) { + const payload = { + event: 'bidWon', + requestId: bid.auctionId, + siteId: bid.params ? [bid.params[0].siteId] : [], + slotId: bid.params ? [bid.params[0].id] : [], + cpm: bid.cpm, + creativeId: bid.creativeId, + adomain: (bid.meta && bid.meta.advertiserDomains) ? bid.meta.advertiserDomains[0] : '', + networkName: (bid.meta && bid.meta.networkName) ? bid.meta.networkName : '', + } + sendNotification(payload); + return payload; + } }, }; diff --git a/modules/synacormediaBidAdapter.js b/modules/synacormediaBidAdapter.js index e0d017a6c51..6608107c93f 100644 --- a/modules/synacormediaBidAdapter.js +++ b/modules/synacormediaBidAdapter.js @@ -37,7 +37,7 @@ export const spec = { const openRtbBidRequest = { id: bidderRequest.auctionId, site: { - domain: location.hostname, + domain: config.getConfig('publisherDomain') || location.hostname, page: refererInfo.referer, ref: document.referrer }, diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 6d55aabbfb5..aad7f6762c4 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -1,6 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const utils = require('../src/utils.js'); const BIDDER_CODE = 'teads'; +const GVL_ID = 132; const ENDPOINT_URL = 'https://a.teads.tv/hb/bid-request'; const gdprStatus = { GDPR_APPLIES_PUBLISHER: 12, @@ -11,6 +12,7 @@ const gdprStatus = { export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: ['video', 'banner'], /** * Determines whether or not the given bid request is valid. diff --git a/modules/telariaBidAdapter.js b/modules/telariaBidAdapter.js index acc20f6b183..71651b5af94 100644 --- a/modules/telariaBidAdapter.js +++ b/modules/telariaBidAdapter.js @@ -124,7 +124,7 @@ function getDefaultSrcPageUrl() { } function getEncodedValIfNotEmpty(val) { - return !utils.isEmpty(val) ? encodeURIComponent(val) : ''; + return (val !== '' && val !== undefined) ? encodeURIComponent(val) : ''; } /** diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index b003de7785f..d54d76efb41 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -3,20 +3,17 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; +const GVLID = 28; const BIDDER_CODE = 'triplelift'; const STR_ENDPOINT = 'https://tlx.3lift.com/header/auction?'; let gdprApplies = true; let consentString = null; export const tripleliftAdapterSpec = { - + gvlid: GVLID, code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function (bid) { - if (bid.mediaTypes.video) { - let video = _getORTBVideo(bid); - if (!video.w || !video.h) return false; - } return typeof bid.params.inventoryCode !== 'undefined'; }, @@ -119,7 +116,8 @@ function _buildPostBody(bidRequests) { tagid: bidRequest.params.inventoryCode, floor: _getFloor(bidRequest) }; - if (bidRequest.mediaTypes.video) { + // remove the else to support multi-imp + if (_isInstreamBidRequest(bidRequest)) { imp.video = _getORTBVideo(bidRequest); } else if (bidRequest.mediaTypes.banner) { imp.banner = { format: _sizes(bidRequest.sizes) }; @@ -147,6 +145,16 @@ function _buildPostBody(bidRequests) { return data; } +function _isInstreamBidRequest(bidRequest) { + if (!bidRequest.mediaTypes.video) return false; + if (!bidRequest.mediaTypes.video.context) return false; + if (bidRequest.mediaTypes.video.context.toLowerCase() === 'instream') { + return true; + } else { + return false; + } +} + function _getORTBVideo(bidRequest) { // give precedent to mediaTypes.video let video = { ...bidRequest.params.video, ...bidRequest.mediaTypes.video }; @@ -277,7 +285,7 @@ function _buildResponseObject(bidderRequest, bid) { meta: {} }; - if (breq.mediaTypes.video) { + if (_isInstreamBidRequest(breq)) { bidResponse.vastXml = bid.ad; bidResponse.mediaType = 'video'; }; @@ -285,6 +293,10 @@ function _buildResponseObject(bidderRequest, bid) { if (bid.advertiser_name) { bidResponse.meta.advertiserName = bid.advertiser_name; } + + if (bid.adomain && bid.adomain.length) { + bidResponse.meta.advertiserDomains = bid.adomain; + } }; return bidResponse; } diff --git a/modules/userId/eids.js b/modules/userId/eids.js index f6c58a5a0bf..2c627416341 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -157,6 +157,18 @@ const USER_IDS_CONFIG = { source: 'idx.lat', atype: 1 }, + + // Verizon Media + 'vmuid': { + source: 'verizonmedia.com', + atype: 1 + }, + + // Neustar Fabrick + 'fabrickId': { + source: 'neustar.biz', + atype: 1 + } }; // this function will create an eid object for the given UserId sub-module diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 7dc149cd47a..0cf9b6d2d22 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -21,6 +21,14 @@ userIdAsEids = [ }] }, + { + source: 'neustar.biz', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { source: 'id5-sync.com', uids: [{ @@ -90,6 +98,7 @@ userIdAsEids = [ atype: 1 }] }, + { source: 'sharedid.org', uids: [{ @@ -100,6 +109,7 @@ userIdAsEids = [ } }] }, + { source: 'zeotap.com', uids: [{ @@ -107,6 +117,7 @@ userIdAsEids = [ atype: 1 }] }, + { source: 'audigent.com', uids: [{ @@ -114,12 +125,21 @@ userIdAsEids = [ atype: 1 }] }, + { source: 'quantcast.com', uids: [{ id: 'some-random-id-value', atype: 1 }] + }, + + { + source: 'verizonmedia.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] } ] ``` diff --git a/modules/userId/index.js b/modules/userId/index.js index 83573be8682..f063fbc973b 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -83,9 +83,9 @@ * @property {(string|undefined)} publisherId - the unique identifier of the publisher in question * @property {(string|undefined)} ajaxTimeout - the number of milliseconds a resolution request can take before automatically being terminated * @property {(array|undefined)} identifiersToResolve - the identifiers from either ls|cookie to be attached to the getId query - * @property {(string|undefined)} providedIdentifierName - defines the name of an identifier that can be found in local storage or in the cookie jar that can be sent along with the getId request. This parameter should be used whenever a customer is able to provide the most stable identifier possible * @property {(LiveIntentCollectConfig|undefined)} liCollectConfig - the config for LiveIntent's collect requests * @property {(string|undefined)} pd - publisher provided data for reconciling ID5 IDs + * @property {(string|undefined)} emailHash - if provided, the hashed email address of a user */ /** @@ -134,6 +134,7 @@ const CONSENT_DATA_COOKIE_STORAGE_CONFIG = { name: '_pbjs_userid_consent_data', expires: 30 // 30 days expiration, which should match how often consent is refreshed by CMPs }; +export const PBJS_USER_ID_OPTOUT_NAME = '_pbjs_id_optout'; export const coreStorage = getCoreStorageManager('userid'); /** @type {string[]} */ @@ -319,7 +320,13 @@ function hasGDPRConsent(consentData) { * @param {function} cb - callback for after processing is done. */ function processSubmoduleCallbacks(submodules, cb) { - const done = cb ? utils.delayExecution(cb, submodules.length) : function () { }; + let done = () => {}; + if (cb) { + done = utils.delayExecution(() => { + clearTimeout(timeoutID); + cb(); + }, submodules.length); + } submodules.forEach(function (submodule) { submodule.callback(function callbackCompleted(idObj) { // if valid, id data should be saved to cookie/html storage @@ -338,7 +345,6 @@ function processSubmoduleCallbacks(submodules, cb) { // clear callback, this prop is used to test if all submodule callbacks are complete below submodule.callback = undefined; }); - clearTimeout(timeoutID); } /** @@ -662,7 +668,7 @@ function updateSubmodules() { if (!addedUserIdHook && submodules.length) { // priority value 40 will load after consentManagement with a priority of 50 getGlobal().requestBids.before(requestBidsHook, 40); - utils.logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules`); + utils.logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules: `, submodules.map(a => a.submodule.name)); addedUserIdHook = true; } } @@ -696,15 +702,15 @@ export function init(config) { ].filter(i => i !== null); // exit immediately if opt out cookie or local storage keys exists. - if (validStorageTypes.indexOf(COOKIE) !== -1 && (coreStorage.getCookie('_pbjs_id_optout') || coreStorage.getCookie('_pubcid_optout'))) { + if (validStorageTypes.indexOf(COOKIE) !== -1 && coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) { utils.logInfo(`${MODULE_NAME} - opt-out cookie found, exit module`); return; } - // _pubcid_optout is checked for compatibility with pubCommonId - if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && (coreStorage.getDataFromLocalStorage('_pbjs_id_optout') || coreStorage.getDataFromLocalStorage('_pubcid_optout'))) { + if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) { utils.logInfo(`${MODULE_NAME} - opt-out localStorage found, exit module`); return; } + // listen for config userSyncs to be set config.getConfig(conf => { // Note: support for 'usersync' was dropped as part of Prebid.js 4.0 diff --git a/modules/userId/userId.md b/modules/userId/userId.md index c46f67f9a9a..267b3a60cea 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -29,9 +29,9 @@ pbjs.setConfig({ pd: "some-pd-string" // See https://wiki.id5.io/x/BIAZ for details }, storage: { - type: "cookie", - name: "id5id.1st", - expires: 90, // Expiration of cookies in days + type: "html5", // ID5 requires html5 + name: "id5id", + expires: 90, // Expiration in days refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires' }, }, { @@ -144,8 +144,8 @@ pbjs.setConfig({ }, storage: { type: 'html5', - name: 'id5id.1st', - expires: 90, // Expiration of cookies in days + name: 'id5id', + expires: 90, // Expiration in days refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires' }, }, { diff --git a/modules/userIdTargeting.js b/modules/userIdTargeting.js index 3ed8b2a14b5..e15c9ddaca2 100644 --- a/modules/userIdTargeting.js +++ b/modules/userIdTargeting.js @@ -20,14 +20,16 @@ export function userIdTargeting(userIds, config) { if (!SHARE_WITH_GAM) { logInfo(MODULE_NAME + ': Not enabled for ' + GAM); - } - - if (window.googletag && isFn(window.googletag.pubads) && hasOwn(window.googletag.pubads(), 'setTargeting') && isFn(window.googletag.pubads().setTargeting)) { + } else if (window.googletag && isFn(window.googletag.pubads) && hasOwn(window.googletag.pubads(), 'setTargeting') && isFn(window.googletag.pubads().setTargeting)) { GAM_API = window.googletag.pubads().setTargeting; } else { - SHARE_WITH_GAM = false; - logInfo(MODULE_NAME + ': Could not find googletag.pubads().setTargeting API. Not adding User Ids in targeting.') - return; + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + GAM_API = function (key, value) { + window.googletag.cmd.push(function () { + window.googletag.pubads().setTargeting(key, value); + }); + }; } Object.keys(userIds).forEach(function(key) { diff --git a/modules/verizonMediaIdSystem.js b/modules/verizonMediaIdSystem.js new file mode 100644 index 00000000000..617561765cc --- /dev/null +++ b/modules/verizonMediaIdSystem.js @@ -0,0 +1,103 @@ +/** + * This module adds verizonMediaId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/verizonMediaIdSystem + * @requires module:modules/userId + */ + +import {ajax} from '../src/ajax.js'; +import {submodule} from '../src/hook.js'; +import * as utils from '../src/utils.js'; + +const MODULE_NAME = 'verizonMediaId'; +const VENDOR_ID = 25; +const PLACEHOLDER = '__PIXEL_ID__'; +const VMUID_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`; + +function isEUConsentRequired(consentData) { + return !!(consentData && consentData.gdpr && consentData.gdpr.gdprApplies); +} + +/** @type {Submodule} */ +export const verizonMediaIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * Vendor id of Verizon Media EMEA Limited + * @type {Number} + */ + gvlid: VENDOR_ID, + /** + * decode the stored id value for passing to bid requests + * @function + * @returns {{vmuid: string} | undefined} + */ + decode(value) { + return (value && typeof value.vmuid === 'string') ? {vmuid: value.vmuid} : undefined; + }, + /** + * get the VerizonMedia Id + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @returns {IdResponse|undefined} + */ + getId(config, consentData) { + const params = config.params || {}; + if (!params || typeof params.he !== 'string' || + (typeof params.pixelId === 'undefined' && typeof params.endpoint === 'undefined')) { + utils.logError('The verizonMediaId submodule requires the \'he\' and \'pixelId\' parameters to be defined.'); + return; + } + + const data = { + '1p': [1, '1', true].includes(params['1p']) ? '1' : '0', + he: params.he, + gdpr: isEUConsentRequired(consentData) ? '1' : '0', + euconsent: isEUConsentRequired(consentData) ? consentData.gdpr.consentString : '', + us_privacy: consentData && consentData.uspConsent ? consentData.uspConsent : '' + }; + + if (params.pixelId) { + data.pixelId = params.pixelId + } + + 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}: ID fetch encountered an error`, error); + callback(); + } + }; + const endpoint = VMUID_ENDPOINT.replace(PLACEHOLDER, params.pixelId); + let url = `${params.endpoint || endpoint}?${utils.formatQS(data)}`; + verizonMediaIdSubmodule.getAjaxFn()(url, callbacks, null, {method: 'GET', withCredentials: true}); + }; + return {callback: resp}; + }, + + /** + * Return the function used to perform XHR calls. + * Utilised for each of testing. + * @returns {Function} + */ + getAjaxFn() { + return ajax; + } +}; + +submodule('userId', verizonMediaIdSubmodule); diff --git a/modules/verizonMediaSystemId.md b/modules/verizonMediaSystemId.md new file mode 100644 index 00000000000..8d0e0bddaa9 --- /dev/null +++ b/modules/verizonMediaSystemId.md @@ -0,0 +1,33 @@ +## Verizon Media User ID Submodule + +Verizon Media User ID Module. + +### Prebid Params + +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'verizonMediaId', + storage: { + name: 'vmuid', + type: 'html5', + expires: 30 + }, + params: { + pixelId: 58776, + he: '0bef996248d63cea1529cb86de31e9547a712d9f380146e98bbd39beec70355a' + } + }] + } +}); +``` +## Parameter Descriptions for the `usersync` Configuration Section +The below parameters apply only to the Verizon Media User ID Module integration. + +| Param under usersync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID value for the Verizon Media module - `"verizonMediaId"` | `"verizonMediaId"` | +| params | Required | Object | Data for Verizon Media ID initialization. | | +| params.pixelId | Required | Number | The Verizon Media supplied publisher specific pixel Id | `8976` | +| params.he | Required | String | The SHA-256 hashed user email address | `"529cb86de31e9547a712d9f380146e98bbd39beec"` | diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index 4b3b1767cec..7fc6e3a5395 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -3,7 +3,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; -const GLVID = 744; +const GVLID = 744; const DEFAULT_SUB_DOMAIN = 'prebid'; const BIDDER_CODE = 'vidazoo'; const BIDDER_VERSION = '1.0.0'; @@ -24,7 +24,7 @@ export const SUPPORTED_ID_SYSTEMS = { 'pubcid': 1, 'tdid': 1, }; -const storage = getStorageManager(GLVID); +const storage = getStorageManager(GVLID); export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { return `https://${subDomain}.cootlogix.com`; @@ -266,6 +266,7 @@ export function tryParseJSON(value) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, version: BIDDER_VERSION, supportedMediaTypes: [BANNER], isBidRequestValid, diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index b252c0db2ee..5465a10a884 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -240,7 +240,7 @@ function createTargetingString (obj) { */ function createSchainString (schain) { const ver = schain.ver || '' - const complete = schain.complete || '' + const complete = (schain.complete === 1 || schain.complete === 0) ? schain.complete : '' const keys = ['asi', 'sid', 'hp', 'rid', 'name', 'domain', 'ext'] const nodesString = schain.nodes.reduce((acc, node) => { return acc += `!${keys.map(key => node[key] ? encodeURIComponentWithBangIncluded(node[key]) : '').join(',')}` diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 08dc3189eda..05af0bf0d66 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -1,103 +1,133 @@ import * as utils from '../src/utils.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import includes from 'core-js-pure/features/array/includes'; const BIDDER_CODE = 'yieldmo'; const CURRENCY = 'USD'; const TIME_TO_LIVE = 300; const NET_REVENUE = true; -const SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; +const BANNER_SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; +const VIDEO_SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebidvideo'; +const OPENRTB_VIDEO_BIDPARAMS = ['placement', 'startdelay', 'skipafter', + 'protocols', 'api', 'playbackmethod', 'maxduration', 'minduration', 'pos']; +const OPENRTB_VIDEO_SITEPARAMS = ['name', 'domain', 'cat', 'keywords']; const localWindow = utils.getWindowTop(); export const spec = { code: BIDDER_CODE, - supportedMediaTypes: ['banner'], + supportedMediaTypes: [BANNER, VIDEO], + /** * Determines whether or not the given bid request is valid. * @param {object} bid, bid to validate * @return boolean, true if valid, otherwise false */ isBidRequestValid: function (bid) { - return !!(bid && bid.adUnitCode && bid.bidId); + return !!(bid && bid.adUnitCode && bid.bidId && (hasBannerMediaType(bid) || hasVideoMediaType(bid)) && + validateVideoParams(bid)); }, + /** * Make a server request from the list of BidRequests. * * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @param {BidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { - let serverRequest = { - pbav: '$prebid.version$', - p: [], - page_url: bidderRequest.refererInfo.referer, - bust: new Date().getTime().toString(), - pr: bidderRequest.refererInfo.referer, - scrd: localWindow.devicePixelRatio || 0, - dnt: getDNT(), - description: getPageDescription(), - title: localWindow.document.title || '', - w: localWindow.innerWidth, - h: localWindow.innerHeight, - userConsent: JSON.stringify({ - // case of undefined, stringify will remove param - gdprApplies: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', - cmp: utils.deepAccess(bidderRequest, 'gdprConsent.consentString') || '' - }), - us_privacy: utils.deepAccess(bidderRequest, 'uspConsent') || '' - }; - - const mtp = window.navigator.maxTouchPoints; - if (mtp) { - serverRequest.mtp = mtp; - } + const bannerBidRequests = bidRequests.filter(request => hasBannerMediaType(request)); + const videoBidRequests = bidRequests.filter(request => hasVideoMediaType(request)); + + let serverRequests = []; + if (bannerBidRequests.length > 0) { + let serverRequest = { + pbav: '$prebid.version$', + p: [], + page_url: bidderRequest.refererInfo.referer, + bust: new Date().getTime().toString(), + pr: bidderRequest.refererInfo.referer, + scrd: localWindow.devicePixelRatio || 0, + dnt: getDNT(), + description: getPageDescription(), + title: localWindow.document.title || '', + w: localWindow.innerWidth, + h: localWindow.innerHeight, + userConsent: JSON.stringify({ + // case of undefined, stringify will remove param + gdprApplies: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', + cmp: utils.deepAccess(bidderRequest, 'gdprConsent.consentString') || '' + }), + us_privacy: utils.deepAccess(bidderRequest, 'uspConsent') || '' + }; - bidRequests.forEach(request => { - serverRequest.p.push(addPlacement(request)); - const pubcid = getId(request, 'pubcid'); - if (pubcid) { - serverRequest.pubcid = pubcid; - } else if (request.crumbs) { - if (request.crumbs.pubcid) { + const mtp = window.navigator.maxTouchPoints; + if (mtp) { + serverRequest.mtp = mtp; + } + + bannerBidRequests.forEach(request => { + serverRequest.p.push(addPlacement(request)); + const pubcid = getId(request, 'pubcid'); + if (pubcid) { + serverRequest.pubcid = pubcid; + } else if (request.crumbs && request.crumbs.pubcid) { serverRequest.pubcid = request.crumbs.pubcid; } - } - const tdid = getId(request, 'tdid'); - if (tdid) { - serverRequest.tdid = tdid; - } - const criteoId = getId(request, 'criteoId'); - if (criteoId) { - serverRequest.cri_prebid = criteoId; - } - if (request.schain) { - serverRequest.schain = - JSON.stringify(request.schain); - } - }); - serverRequest.p = '[' + serverRequest.p.toString() + ']'; - return { - method: 'GET', - url: SERVER_ENDPOINT, - data: serverRequest - }; + const tdid = getId(request, 'tdid'); + if (tdid) { + serverRequest.tdid = tdid; + } + const criteoId = getId(request, 'criteoId'); + if (criteoId) { + serverRequest.cri_prebid = criteoId; + } + if (request.schain) { + serverRequest.schain = JSON.stringify(request.schain); + } + }); + serverRequest.p = '[' + serverRequest.p.toString() + ']'; + serverRequests.push({ + method: 'GET', + url: BANNER_SERVER_ENDPOINT, + data: serverRequest + }); + } + + if (videoBidRequests.length > 0) { + const serverRequest = openRtbRequest(videoBidRequests, bidderRequest); + serverRequests.push({ + method: 'POST', + url: VIDEO_SERVER_ENDPOINT, + data: serverRequest + }); + } + return serverRequests; }, + /** * Makes Yieldmo Ad Server response compatible to Prebid specs - * @param serverResponse successful response from Ad Server + * @param {ServerResponse} serverResponse successful response from Ad Server + * @param {ServerRequest} bidRequest * @return {Bid[]} an array of bids */ - interpretResponse: function (serverResponse) { + interpretResponse: function (serverResponse, bidRequest) { let bids = []; - let data = serverResponse.body; + const data = serverResponse.body; if (data.length > 0) { data.forEach(response => { - if (response.cpm && response.cpm > 0) { - bids.push(createNewBid(response)); + if (response.cpm > 0) { + bids.push(createNewBannerBid(response)); } }); } + if (data.seatbid) { + const seatbids = data.seatbid.reduce((acc, seatBid) => acc.concat(seatBid.bid), []); + seatbids.forEach(bid => bids.push(createNewVideoBid(bid, bidRequest))); + } return bids; }, + getUserSyncs: function () { return []; } @@ -108,6 +138,20 @@ registerBidder(spec); * Helper Functions ***************************************/ +/** + * @param {BidRequest} bidRequest bid request + */ +function hasBannerMediaType(bidRequest) { + return !!utils.deepAccess(bidRequest, 'mediaTypes.banner'); +} + +/** + * @param {BidRequest} bidRequest bid request + */ +function hasVideoMediaType(bidRequest) { + return !!utils.deepAccess(bidRequest, 'mediaTypes.video'); +} + /** * Adds placement information to array * @param request bid request @@ -130,10 +174,10 @@ function addPlacement(request) { } /** - * creates a new bid with response information + * creates a new banner bid with response information * @param response server response */ -function createNewBid(response) { +function createNewBannerBid(response) { return { requestId: response['callback_id'], cpm: response.cpm, @@ -147,6 +191,27 @@ function createNewBid(response) { }; } +/** + * creates a new video bid with response information + * @param response openRTB server response + * @param bidRequest server request + */ +function createNewVideoBid(response, bidRequest) { + const imp = (utils.deepAccess(bidRequest, 'data.imp') || []).find(imp => imp.id === response.impid); + return { + requestId: imp.id, + cpm: response.price, + width: imp.video.w, + height: imp.video.h, + creativeId: response.crid || response.adid, + currency: CURRENCY, + netRevenue: NET_REVENUE, + mediaType: VIDEO, + ttl: TIME_TO_LIVE, + vastXml: response.adm + }; +} + /** * Detects whether dnt is true * @returns true if user enabled dnt @@ -179,3 +244,215 @@ function getPageDescription() { function getId(request, idType) { return (typeof utils.deepAccess(request, 'userId') === 'object') ? request.userId[idType] : undefined; } + +/** + * @param {BidRequest[]} bidRequests bid request object + * @param {BidderRequest} bidderRequest bidder request object + * @return Object OpenRTB request object + */ +function openRtbRequest(bidRequests, bidderRequest) { + let openRtbRequest = { + id: bidRequests[0].bidderRequestId, + at: 1, + imp: bidRequests.map(bidRequest => openRtbImpression(bidRequest)), + site: openRtbSite(bidRequests[0], bidderRequest), + device: openRtbDevice(), + badv: bidRequests[0].params.badv || [], + bcat: bidRequests[0].params.bcat || [], + ext: { + prebid: '$prebid.version$', + } + }; + + populateOpenRtbGdpr(openRtbRequest, bidderRequest); + + return openRtbRequest; +} + +/** + * @param {BidRequest} bidRequest bidder request object. + * @return Object OpenRTB's 'imp' (impression) object + */ +function openRtbImpression(bidRequest) { + const videoReq = utils.deepAccess(bidRequest, 'mediaTypes.video'); + const size = extractPlayerSize(bidRequest); + const imp = { + id: bidRequest.bidId, + tagid: bidRequest.adUnitCode, + bidfloor: bidRequest.params.bidfloor || 0, + ext: { + placement_id: bidRequest.params.placementId + }, + video: { + w: size[0], + h: size[1], + mimes: videoReq.mimes, + linearity: 1 + } + }; + + const videoParams = utils.deepAccess(bidRequest, 'params.video'); + Object.keys(videoParams) + .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) + .forEach(param => imp.video[param] = videoParams[param]); + + if (videoParams.skippable) { + imp.video.skip = 1; + } + + return imp; +} + +/** + * @param {BidRequest} bidRequest bidder request object. + * @return [number, number] || null Player's width and height, or undefined otherwise. + */ +function extractPlayerSize(bidRequest) { + const sizeArr = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + if (utils.isArrayOfNums(sizeArr, 2)) { + return sizeArr; + } else if (utils.isArray(sizeArr) && utils.isArrayOfNums(sizeArr[0], 2)) { + return sizeArr[0]; + } + return null; +} + +/** + * @param {BidRequest} bidRequest bid request object + * @param {BidderRequest} bidderRequest bidder request object + * @return Object OpenRTB's 'site' object + */ +function openRtbSite(bidRequest, bidderRequest) { + let result = {}; + + const loc = utils.parseUrl(utils.deepAccess(bidderRequest, 'refererInfo.referer')); + if (!utils.isEmpty(loc)) { + result.page = `${loc.protocol}://${loc.hostname}${loc.pathname}`; + } + + if (self === top && document.referrer) { + result.ref = document.referrer; + } + + const keywords = document.getElementsByTagName('meta')['keywords']; + if (keywords && keywords.content) { + result.keywords = keywords.content; + } + + const siteParams = utils.deepAccess(bidRequest, 'params.site'); + if (siteParams) { + Object.keys(siteParams) + .filter(param => includes(OPENRTB_VIDEO_SITEPARAMS, param)) + .forEach(param => result[param] = siteParams[param]); + } + return result; +} + +/** + * @return Object OpenRTB's 'device' object + */ +function openRtbDevice() { + return { + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + }; +} + +/** + * Updates openRtbRequest with GDPR info from bidderRequest, if present. + * @param {Object} openRtbRequest OpenRTB's request to update. + * @param {BidderRequest} bidderRequest bidder request object. + */ +function populateOpenRtbGdpr(openRtbRequest, bidderRequest) { + const gdpr = bidderRequest.gdprConsent; + if (gdpr && 'gdprApplies' in gdpr) { + utils.deepSetValue(openRtbRequest, 'regs.ext.gdpr', gdpr.gdprApplies ? 1 : 0); + utils.deepSetValue(openRtbRequest, 'user.ext.consent', gdpr.consentString); + } + const uspConsent = utils.deepAccess(bidderRequest, 'uspConsent'); + if (uspConsent) { + utils.deepSetValue(openRtbRequest, 'regs.ext.us_privacy', uspConsent); + } +} + +/** + * Determines whether or not the given video bid request is valid. If it's not a video bid, returns true. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ +function validateVideoParams(bid) { + if (!hasVideoMediaType(bid)) { + return true; + } + + const paramRequired = (paramStr, value, conditionStr) => { + let error = `"${paramStr}" is required`; + if (conditionStr) { + error += ' when ' + conditionStr; + } + throw new Error(error); + } + + const paramInvalid = (paramStr, value, expectedStr) => { + expectedStr = expectedStr ? ', expected: ' + expectedStr : ''; + value = JSON.stringify(value); + throw new Error(`"${paramStr}"=${value} is invalid${expectedStr}`); + } + + const isDefined = val => typeof val !== 'undefined'; + const validate = (fieldPath, validateCb, errorCb, errorCbParam) => { + const value = utils.deepAccess(bid, fieldPath); + if (!validateCb(value)) { + errorCb(fieldPath, value, errorCbParam); + } + return value; + } + + try { + validate('params.placementId', val => !utils.isEmpty(val), paramRequired); + + validate('mediaTypes.video.playerSize', val => utils.isArrayOfNums(val, 2) || + (utils.isArray(val) && val.every(v => utils.isArrayOfNums(v, 2))), + paramInvalid, 'array of 2 integers, ex: [640,480] or [[640,480]]'); + + validate('mediaTypes.video.mimes', val => isDefined(val), paramRequired); + validate('mediaTypes.video.mimes', val => utils.isArray(val) && val.every(v => utils.isStr(v)), paramInvalid, + 'array of strings, ex: ["video/mp4"]'); + + validate('params.video', val => !utils.isEmpty(val), paramRequired); + + const placement = validate('params.video.placement', val => isDefined(val), paramRequired); + validate('params.video.placement', val => val >= 1 && val <= 5, paramInvalid); + if (placement === 1) { + validate('params.video.startdelay', val => isDefined(val), + (field, v) => paramRequired(field, v, 'placement == 1')); + validate('params.video.startdelay', val => utils.isNumber(val), paramInvalid, 'number, ex: 5'); + } + + validate('params.video.protocols', val => isDefined(val), paramRequired); + validate('params.video.protocols', val => utils.isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), + paramInvalid, 'array of numbers, ex: [2,3]'); + + validate('params.video.api', val => isDefined(val), paramRequired); + validate('params.video.api', val => utils.isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), + paramInvalid, 'array of numbers, ex: [2,3]'); + + validate('params.video.playbackmethod', val => !isDefined(val) || utils.isArrayOfNums(val), paramInvalid, + 'array of integers, ex: [2,6]'); + + validate('params.video.maxduration', val => isDefined(val), paramRequired); + validate('params.video.maxduration', val => utils.isInteger(val), paramInvalid); + validate('params.video.minduration', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.video.skippable', val => !isDefined(val) || utils.isBoolean(val), paramInvalid); + validate('params.video.skipafter', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.video.pos', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.badv', val => !isDefined(val) || utils.isArray(val), paramInvalid, + 'array of strings, ex: ["ford.com","pepsi.com"]'); + validate('params.bcat', val => !isDefined(val) || utils.isArray(val), paramInvalid, + 'array of strings, ex: ["IAB1-5","IAB1-6"]'); + return true; + } catch (e) { + utils.logError(e.message); + return false; + } +} diff --git a/modules/yieldmoBidAdapter.md b/modules/yieldmoBidAdapter.md index 0f86d2507d1..1b8b7b1b741 100644 --- a/modules/yieldmoBidAdapter.md +++ b/modules/yieldmoBidAdapter.md @@ -11,26 +11,61 @@ Note: Our ads will only render in mobile Connects to Yieldmo Ad Server for bids. -Yieldmo bid adapter supports Banner. +Yieldmo bid adapter supports Banner and Video. # Test Parameters + +## Banner + +Sample banner ad unit config: +```javascript +var adUnits = [{ // Banner adUnit + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bids: [{ + bidder: 'yieldmo', + params: { + placementId: '1779781193098233305', // string with at most 19 characters (may include numbers only) + bidFloor: .28 // optional param + } + }] +}]; +``` + +## Video + +Sample instream video ad unit config: +```javascript +var adUnits = [{ // Video adUnit + code: 'div-video-ad-1234567890', + mediaTypes: { + video: { + playerSize: [640, 480], // required + context: 'instream', + mimes: ['video/mp4'] // required, array of strings + } + }, + bids: [{ + bidder: 'yieldmo', + params: { + placementId: '1524592390382976659', // required + video: { + placement: 1, // required, integer + maxduration: 30, // required, integer + minduration: 15, // optional, integer + pos: 1, // optional, integer + startdelay: 10, // required if placement == 1 + protocols: [2, 3], // required, array of integers + api: [2, 3], // required, array of integers + playbackmethod: [2,6], // required, array of integers + skippable: true, // optional, boolean + skipafter: 10 // optional, integer + } + } + }] +}]; ``` -var adUnits = [ - // Banner adUnit - { - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]], - } - } - bids: [{ - bidder: 'yieldmo', - params: { - placementId: '1779781193098233305', // string with at most 19 characters (may include numbers only) - bidFloor: .28 // optional param - } - }] - } -]; -``` \ No newline at end of file diff --git a/modules/zetaBidAdapter.js b/modules/zetaBidAdapter.js new file mode 100644 index 00000000000..f60e8946799 --- /dev/null +++ b/modules/zetaBidAdapter.js @@ -0,0 +1,161 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +const BIDDER_CODE = 'zeta_global'; +const ENDPOINT_URL = 'https://prebid.rfihub.com/prebid'; +const USER_SYNC_URL = 'https://p.rfihub.com/cm?pub=42770&in=1'; +const DEFAULT_CUR = 'USD'; +const TTL = 200; +const NET_REV = true; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + // check for all required bid fields + let isValid = !!( + bid && + bid.bidId && + bid.params && + bid.params.ip && + bid.params.user && + bid.params.user.buyeruid && + bid.params.definerId + ); + if (!isValid) { + utils.logWarn('Invalid bid request'); + } + return isValid; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {Bids[]} validBidRequests - an array of bidRequest objects + * @param {BidderRequest} bidderRequest - master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const secure = 1; // treat all requests as secure + const request = validBidRequests[0]; + const params = request.params; + let impData = { + id: request.bidId, + secure: secure, + banner: buildBanner(request) + }; + let isMobile = /(ios|ipod|ipad|iphone|android)/i.test(navigator.userAgent) ? 1 : 0; + let payload = { + id: bidderRequest.auctionId, + cur: [DEFAULT_CUR], + imp: [impData], + site: { + mobile: isMobile, + page: bidderRequest.refererInfo.referer + }, + device: { + ua: navigator.userAgent, + ip: params.ip + }, + user: { + buyeruid: params.user.buyeruid, + uid: params.user.uid + }, + ext: { + definerId: params.definerId + } + }; + if (params.test) { + payload.test = params.test; + } + if (request.gdprConsent) { + payload.regs = { + ext: { + gdpr: request.gdprConsent.gdprApplies === true ? 1 : 0 + } + }; + } + if (request.gdprConsent && request.gdprConsent.gdprApplies) { + payload.user = { + ext: { + consent: request.gdprConsent.consentString + } + }; + } + return { + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(payload), + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param bidRequest The payload from the server's response. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + let bidResponse = []; + if (Object.keys(serverResponse.body).length !== 0) { + let zetaResponse = serverResponse.body; + let zetaBid = zetaResponse.seatbid[0].bid[0]; + let bid = { + requestId: zetaBid.impid, + cpm: zetaBid.price, + currency: zetaResponse.cur, + width: zetaBid.w, + height: zetaBid.h, + ad: zetaBid.adm, + ttl: TTL, + creativeId: zetaBid.crid, + netRevenue: NET_REV + }; + bidResponse.push(bid); + } + return bidResponse; + }, + + /** + * 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 The GDPR consent parameters + * @param uspConsent The USP consent parameters + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: USER_SYNC_URL + }); + } + return syncs; + } +} + +function buildBanner(request) { + let sizes = request.sizes; + if (request.mediaTypes && + request.mediaTypes.banner && + request.mediaTypes.banner.sizes) { + sizes = request.mediaTypes.banner.sizes; + } + return { + w: sizes[0][0], + h: sizes[0][1] + }; +} + +registerBidder(spec); diff --git a/modules/zetaBidAdapter.md b/modules/zetaBidAdapter.md new file mode 100644 index 00000000000..ce19b831d4d --- /dev/null +++ b/modules/zetaBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +``` +Module Name: Zeta Bidder Adapter +Module Type: Bidder Adapter +Maintainer: DL-ZetaDSP-Supply-Engineering@zetaglobal.com +``` + +# Description + +Module that connects to Zeta's demand sources + +# Test Parameters +``` + var adUnits = [ + { + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: 'zeta_global', + bidId: 12345, + params: { + placement: 12345, + user: { + uid: 12345, + buyeruid: 12345 + }, + ip: '111.222.33.44', + definerId: 1, + test: 1 + } + } + ] + } + ]; +``` diff --git a/package-lock.json b/package-lock.json index 1784b885be9..bf95487ed9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.8.0-pre", + "version": "4.14.0-pre", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7796,13 +7796,24 @@ "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", "object-inspect": "^1.7.0", "object-keys": "^1.1.1", "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + }, + "dependencies": { + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + } } }, "es-array-method-boxes-properly": { @@ -7838,6 +7849,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -12019,6 +12031,12 @@ "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", "dev": true }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -12076,7 +12094,7 @@ "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", "dev": true, "requires": { - "has-symbols": "^1.0.1" + "has": "^1.0.3" } }, "is-relative": { @@ -15709,9 +15727,9 @@ "dev": true }, "live-connect-js": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-1.1.10.tgz", - "integrity": "sha512-G/LJKN3b21DZILCQRyataC/znLvJRyogtu7mAkKlkhP9B9UJ8bcOL7ihW/clD2PsT4hVUkeabHhUGsPCmhsjFw==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-1.1.23.tgz", + "integrity": "sha512-alOXlYyDdMXt8zzCIs3+iCrdi6r/69c7YRN3sMETa3b2cCOxep3i9j2O0iepk2hxT5JxiR1MvqlqdWAL9d2Hcg==", "requires": { "@kiosked/ulid": "^3.0.0", "abab": "^2.0.3", @@ -17484,7 +17502,8 @@ "object-inspect": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true }, "object-is": { "version": "1.1.2", @@ -20697,23 +20716,143 @@ } }, "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } + } + }, + "string.prototype.trimleft": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" + } + }, + "string.prototype.trimright": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" } }, "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } } }, "string_decoder": { diff --git a/package.json b/package.json index 6273d680e19..ed4514cee42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.13.0-pre", + "version": "4.19.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { @@ -112,6 +112,6 @@ "fun-hooks": "^0.9.9", "jsencrypt": "^3.0.0-rc.1", "just-clone": "^1.0.2", - "live-connect-js": "1.1.10" + "live-connect-js": "^1.1.23" } } diff --git a/src/Renderer.js b/src/Renderer.js index f073d97d052..7cedf278537 100644 --- a/src/Renderer.js +++ b/src/Renderer.js @@ -115,5 +115,21 @@ function isRendererPreferredFromAdUnit(adUnitCode) { const adUnit = find(adUnits, adUnit => { return adUnit.code === adUnitCode; }); - return !!(adUnit && adUnit.renderer && adUnit.renderer.url && adUnit.renderer.render && !(utils.isBoolean(adUnit.renderer.backupOnly) && adUnit.renderer.backupOnly)); + + if (!adUnit) { + return false + } + + // renderer defined at adUnit level + const adUnitRenderer = utils.deepAccess(adUnit, 'renderer'); + const hasValidAdUnitRenderer = !!(adUnitRenderer && adUnitRenderer.url && adUnitRenderer.render); + + // renderer defined at adUnit.mediaTypes level + const mediaTypeRenderer = utils.deepAccess(adUnit, 'mediaTypes.video.renderer'); + const hasValidMediaTypeRenderer = !!(mediaTypeRenderer && mediaTypeRenderer.url && mediaTypeRenderer.render) + + return !!( + (hasValidAdUnitRenderer && !(adUnitRenderer.backupOnly === true)) || + (hasValidMediaTypeRenderer && !(mediaTypeRenderer.backupOnly === true)) + ); } diff --git a/src/auction.js b/src/auction.js index 5858c3edf78..c94e3adc9a7 100644 --- a/src/auction.js +++ b/src/auction.js @@ -57,7 +57,7 @@ * @property {function(): void} callBids - sends requests to all adapters for bids */ -import {flatten, timestamp, adUnitsFilter, deepAccess, getBidRequest, getValue, parseUrl, isBoolean} from './utils.js'; +import {flatten, timestamp, adUnitsFilter, deepAccess, getBidRequest, getValue, parseUrl} from './utils.js'; import { getPriceBucketString } from './cpmBucketManager.js'; import { getNativeTargeting } from './native.js'; import { getCacheUrl, store } from './videoCache.js'; @@ -66,6 +66,7 @@ import { config } from './config.js'; import { userSync } from './userSync.js'; import { hook } from './hook.js'; import find from 'core-js-pure/features/array/find.js'; +import includes from 'core-js-pure/features/array/includes.js'; import { OUTSTREAM } from './video.js'; import { VIDEO } from './mediaTypes.js'; @@ -397,10 +398,19 @@ export function auctionCallbacks(auctionDone, auctionInstance) { function adapterDone() { let bidderRequest = this; + let bidderRequests = auctionInstance.getBidRequests(); + const auctionOptionsConfig = config.getConfig('auctionOptions'); bidderRequestsDone.add(bidderRequest); - allAdapterCalledDone = auctionInstance.getBidRequests() - .every(bidderRequest => bidderRequestsDone.has(bidderRequest)); + + if (auctionOptionsConfig && !utils.isEmpty(auctionOptionsConfig)) { + const secondaryBidders = auctionOptionsConfig.secondaryBidders; + if (secondaryBidders && !bidderRequests.every(bidder => includes(secondaryBidders, bidder.bidderCode))) { + bidderRequests = bidderRequests.filter(request => !includes(secondaryBidders, request.bidderCode)); + } + } + + allAdapterCalledDone = bidderRequests.every(bidderRequest => bidderRequestsDone.has(bidderRequest)); bidderRequest.bids.forEach(bid => { if (!bidResponseMap[bid.bidId]) { @@ -512,9 +522,26 @@ function getPreparedBidForAuction({adUnitCode, bid, bidderRequest, auctionId}) { const bidReq = bidderRequest.bids && find(bidderRequest.bids, bid => bid.adUnitCode == adUnitCode); const adUnitRenderer = bidReq && bidReq.renderer; - if (adUnitRenderer && adUnitRenderer.url && !(adUnitRenderer.backupOnly && isBoolean(adUnitRenderer.backupOnly) && bid.renderer)) { - bidObject.renderer = Renderer.install({ url: adUnitRenderer.url }); - bidObject.renderer.setRender(adUnitRenderer.render); + // a publisher can also define a renderer for a mediaType + const bidObjectMediaType = bidObject.mediaType; + const bidMediaType = bidReq && + bidReq.mediaTypes && + bidReq.mediaTypes[bidObjectMediaType]; + + var mediaTypeRenderer = bidMediaType && bidMediaType.renderer; + + var renderer = null; + + // the renderer for the mediaType takes precendence + if (mediaTypeRenderer && mediaTypeRenderer.url && !(mediaTypeRenderer.backupOnly === true && mediaTypeRenderer.render)) { + renderer = mediaTypeRenderer; + } else if (adUnitRenderer && adUnitRenderer.url && !(adUnitRenderer.backupOnly === true && bid.renderer)) { + renderer = adUnitRenderer; + } + + if (renderer) { + bidObject.renderer = Renderer.install({ url: renderer.url }); + bidObject.renderer.setRender(renderer.render); } // Use the config value 'mediaTypeGranularity' if it has been defined for mediaType, else use 'customPriceBucket' diff --git a/src/config.js b/src/config.js index 3284be52296..daaf739bbbd 100644 --- a/src/config.js +++ b/src/config.js @@ -199,6 +199,16 @@ export function newConfig() { set disableAjaxTimeout(val) { this._disableAjaxTimeout = val; }, + + _auctionOptions: {}, + get auctionOptions() { + return this._auctionOptions; + }, + set auctionOptions(val) { + if (validateauctionOptions(val)) { + this._auctionOptions = val; + } + }, }; if (config) { @@ -237,6 +247,30 @@ export function newConfig() { } return true; } + + function validateauctionOptions(val) { + if (!utils.isPlainObject(val)) { + utils.logWarn('Auction Options must be an object') + return false + } + + for (let k of Object.keys(val)) { + if (k !== 'secondaryBidders') { + utils.logWarn(`Auction Options given an incorrect param: ${k}`) + return false + } + if (k === 'secondaryBidders') { + if (!utils.isArray(val[k])) { + utils.logWarn(`Auction Options ${k} must be of type Array`); + return false + } else if (!val[k].every(utils.isStr)) { + utils.logWarn(`Auction Options ${k} must be only string`); + return false + } + } + } + return true; + } } /** diff --git a/src/events.js b/src/events.js index e7a11635476..8749ddf206b 100644 --- a/src/events.js +++ b/src/events.js @@ -44,7 +44,8 @@ module.exports = (function () { eventsFired.push({ eventType: eventString, args: eventPayload, - id: key + id: key, + elapsedTime: utils.getPerformanceNow(), }); /** Push each specific callback to the `callbacks` array. diff --git a/src/prebid.js b/src/prebid.js index 8bfb6024d7a..0f72ca878e5 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -258,6 +258,18 @@ $$PREBID_GLOBAL$$.getNoBids = function () { return getBids('getNoBids'); }; +/** + * This function returns the bids requests involved in an auction but not bid on or the specified adUnitCode + * @param {string} adUnitCode adUnitCode + * @alias module:pbjs.getNoBidsForAdUnitCode + * @return {Object} bidResponse object + */ + +$$PREBID_GLOBAL$$.getNoBidsForAdUnitCode = function (adUnitCode) { + const bids = auctionManager.getNoBids().filter(bid => bid.adUnitCode === adUnitCode); + return { bids }; +}; + /** * This function returns the bid responses at the given moment. * @alias module:pbjs.getBidResponses diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 34de2be275c..cb192dd773e 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -5,14 +5,14 @@ import events from './events.js'; import { fireNativeTrackers, getAssetMessage } from './native.js'; -import { EVENTS } from './constants.json'; +import constants from './constants.json'; import { logWarn, replaceAuctionPrice } from './utils.js'; import { auctionManager } from './auctionManager.js'; import find from 'core-js-pure/features/array/find.js'; import { isRendererRequired, executeRenderer } from './Renderer.js'; import includes from 'core-js-pure/features/array/includes.js'; -const BID_WON = EVENTS.BID_WON; +const BID_WON = constants.EVENTS.BID_WON; export function listenMessagesFromCreative() { window.addEventListener('message', receiveMessage, false); diff --git a/src/targeting.js b/src/targeting.js index 8176bc9caff..b6a38bdbb61 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -235,7 +235,8 @@ export function newTargeting(auctionManager) { // `alwaysUseBid=true`. If sending all bids is enabled, add targeting for losing bids. var targeting = getWinningBidTargeting(adUnitCodes, bidsReceived) .concat(getCustomBidTargeting(adUnitCodes, bidsReceived)) - .concat(config.getConfig('enableSendAllBids') ? getBidLandscapeTargeting(adUnitCodes, bidsReceived) : getDealBids(adUnitCodes, bidsReceived)); + .concat(config.getConfig('enableSendAllBids') ? getBidLandscapeTargeting(adUnitCodes, bidsReceived) : getDealBids(adUnitCodes, bidsReceived)) + .concat(getAdUnitTargeting(adUnitCodes)); // store a reference of the targeting keys targeting.map(adUnitCode => { @@ -609,6 +610,27 @@ export function newTargeting(auctionManager) { }); } + function getAdUnitTargeting(adUnitCodes) { + function getTargetingObj(adUnit) { + return deepAccess(adUnit, CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING); + } + + function getTargetingValues(adUnit) { + const aut = getTargetingObj(adUnit); + + return Object.keys(aut) + .map(function(key) { + return {[key]: utils.isArray(aut[key]) ? aut[key] : aut[key].split(',')}; + }); + } + + return auctionManager.getAdUnits() + .filter(adUnit => includes(adUnitCodes, adUnit.code) && getTargetingObj(adUnit)) + .map(adUnit => { + return {[adUnit.code]: getTargetingValues(adUnit)} + }); + } + targeting.isApntagDefined = function() { if (window.apntag && utils.isFn(window.apntag.setKeywords)) { return true; diff --git a/src/utils.js b/src/utils.js index 8af7a25668d..acdf0f101ad 100644 --- a/src/utils.js +++ b/src/utils.js @@ -727,6 +727,14 @@ export function timestamp() { return new Date().getTime(); } +/** + * The returned value represents the time elapsed since the time origin. @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now + * @returns {number} + */ +export function getPerformanceNow() { + return (window.performance && window.performance.now && window.performance.now()) || 0; +} + /** * When the deviceAccess flag config option is false, no cookies should be read or set * @returns {boolean} diff --git a/src/video.js b/src/video.js index befeb2ded39..20df7a92442 100644 --- a/src/video.js +++ b/src/video.js @@ -59,7 +59,7 @@ export const checkVideoBidSetup = hook('sync', function(bid, bidRequest, videoMe // outstream bids require a renderer on the bid or pub-defined on adunit if (context === OUTSTREAM) { - return !!(bid.renderer || bidRequest.renderer); + return !!(bid.renderer || bidRequest.renderer || videoMediaType.renderer); } return true; diff --git a/test/spec/api_spec.js b/test/spec/api_spec.js index 6f21eba7aaf..6d67565056f 100755 --- a/test/spec/api_spec.js +++ b/test/spec/api_spec.js @@ -43,10 +43,14 @@ describe('Publisher API', function () { assert.isFunction($$PREBID_GLOBAL$$.getBidResponses); }); - it('should have function $$PREBID_GLOBAL$$.getBidResponses', function () { + it('should have function $$PREBID_GLOBAL$$.getNoBids', function () { assert.isFunction($$PREBID_GLOBAL$$.getNoBids); }); + it('should have function $$PREBID_GLOBAL$$.getNoBidsForAdUnitCode', function () { + assert.isFunction($$PREBID_GLOBAL$$.getNoBidsForAdUnitCode); + }); + it('should have function $$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode', function () { assert.isFunction($$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode); }); diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index e35b1406fbf..a50eba5e585 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -763,6 +763,68 @@ describe('auctionmanager.js', function () { assert.equal(addedBid.renderer.url, 'renderer.js'); }); + it('installs publisher-defined renderers for a media type', function () { + const renderer = { + url: 'videoRenderer.js', + render: (bid) => bid + }; + let myBid = mockBid(); + let bidRequest = mockBidRequest(myBid); + + bidRequest.bids[0] = { + ...bidRequest.bids[0], + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'outstream', + renderer + } + } + }; + makeRequestsStub.returns([bidRequest]); + + myBid.mediaType = 'video'; + spec.interpretResponse.returns(myBid); + auction.callBids(); + + const addedBid = auction.getBidsReceived().pop(); + assert.equal(addedBid.renderer.url, renderer.url); + }); + + it('installs bidder-defined renderer when onlyBackup is true in mediaTypes.video options ', function () { + const renderer = { + url: 'videoRenderer.js', + backupOnly: true, + render: (bid) => bid + }; + let myBid = mockBid(); + let bidRequest = mockBidRequest(myBid); + + bidRequest.bids[0] = { + ...bidRequest.bids[0], + mediaTypes: { + video: { + context: 'outstream', + renderer + } + } + }; + makeRequestsStub.returns([bidRequest]); + + myBid.mediaType = 'video'; + myBid.renderer = { + url: 'renderer.js', + render: sinon.spy() + }; + spec.interpretResponse.returns(myBid); + auction.callBids(); + + const addedBid = auction.getBidsReceived().pop(); + assert.strictEqual(addedBid.renderer.url, myBid.renderer.url); + }); + it('bid for a regular unit and a video unit', function() { let renderer = { url: 'renderer.js', @@ -1252,4 +1314,119 @@ describe('auctionmanager.js', function () { assert.equal(doneSpy.callCount, 1); }) }); + + describe('auctionOptions', function() { + let bidRequests; + let doneSpy; + let clock; + let auction = { + getBidRequests: () => bidRequests, + getAuctionId: () => '1', + addBidReceived: () => true, + getTimeout: () => 1000 + } + let requiredBidder = BIDDER_CODE; + let requiredBidder1 = BIDDER_CODE1; + let secondaryBidder = 'doNotWaitForMe'; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + doneSpy = sinon.spy(); + config.setConfig({ + 'auctionOptions': { + secondaryBidders: [ secondaryBidder ] + } + }) + }); + + afterEach(() => { + doneSpy.resetHistory(); + config.resetConfig(); + clock.restore(); + }); + + it('should not wait to call auction done for secondary bidders', function () { + let bids1 = [mockBid({ bidderCode: requiredBidder })]; + let bids2 = [mockBid({ bidderCode: requiredBidder1 })]; + let bids3 = [mockBid({ bidderCode: secondaryBidder })]; + bidRequests = [ + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids3[0], { adUnitCode: ADUNIT_CODE1 }), + ]; + let cbs = auctionCallbacks(doneSpy, auction); + // required bidder responds immeaditely to auction + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[0]); + assert.equal(doneSpy.callCount, 0); + + // auction waits for second required bidder to respond + clock.tick(100); + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids2[0]); + cbs.adapterDone.call(bidRequests[1]); + + // auction done is reported and does not wait for secondaryBidder request + assert.equal(doneSpy.callCount, 1); + + cbs.addBidResponse.call(bidRequests[2], ADUNIT_CODE1, bids3[0]); + cbs.adapterDone.call(bidRequests[2]); + }); + + it('should wait for all bidders if they are all secondary', function () { + config.setConfig({ + 'auctionOptions': { + secondaryBidders: [requiredBidder, requiredBidder1, secondaryBidder] + } + }) + let bids1 = [mockBid({ bidderCode: requiredBidder })]; + let bids2 = [mockBid({ bidderCode: requiredBidder1 })]; + let bids3 = [mockBid({ bidderCode: secondaryBidder })]; + bidRequests = [ + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids3[0], { adUnitCode: ADUNIT_CODE1 }), + ]; + let cbs = auctionCallbacks(doneSpy, auction); + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[0]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0) + + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids2[0]); + cbs.adapterDone.call(bidRequests[1]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0); + + cbs.addBidResponse.call(bidRequests[2], ADUNIT_CODE1, bids3[0]); + cbs.adapterDone.call(bidRequests[2]); + assert.equal(doneSpy.callCount, 1); + }); + + it('should allow secondaryBidders to respond in auction before is is done', function () { + let bids1 = [mockBid({ bidderCode: requiredBidder })]; + let bids2 = [mockBid({ bidderCode: requiredBidder1 })]; + let bids3 = [mockBid({ bidderCode: secondaryBidder })]; + bidRequests = [ + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids3[0], { adUnitCode: ADUNIT_CODE1 }), + ]; + let cbs = auctionCallbacks(doneSpy, auction); + // secondaryBidder is first to respond + cbs.addBidResponse.call(bidRequests[2], ADUNIT_CODE1, bids3[0]); + cbs.adapterDone.call(bidRequests[2]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0); + + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids2[0]); + cbs.adapterDone.call(bidRequests[1]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0); + + // first required bidder takes longest to respond, auction isn't marked as done until this occurs + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[0]); + assert.equal(doneSpy.callCount, 1); + }); + }); }); diff --git a/test/spec/config_spec.js b/test/spec/config_spec.js index be5b4bbb78b..81ce966efb2 100644 --- a/test/spec/config_spec.js +++ b/test/spec/config_spec.js @@ -211,4 +211,37 @@ describe('config API', function () { setConfig({ bidderSequence: 'random' }); expect(logWarnSpy.called).to.equal(false); }); + + it('sets auctionOptions', function () { + const auctionOptionsConfig = { + 'secondaryBidders': ['rubicon', 'appnexus'] + } + setConfig({ auctionOptions: auctionOptionsConfig }); + expect(getConfig('auctionOptions')).to.eql(auctionOptionsConfig); + }); + + it('should log warning for the wrong value passed to auctionOptions', function () { + setConfig({ auctionOptions: '' }); + expect(logWarnSpy.calledOnce).to.equal(true); + const warning = 'Auction Options must be an object'; + assert.ok(logWarnSpy.calledWith(warning), 'expected warning was logged'); + }); + + it('should log warning for invalid auctionOptions bidder values', function () { + setConfig({ auctionOptions: { + 'secondaryBidders': 'appnexus, rubicon', + }}); + expect(logWarnSpy.calledOnce).to.equal(true); + const warning = 'Auction Options secondaryBidders must be of type Array'; + assert.ok(logWarnSpy.calledWith(warning), 'expected warning was logged'); + }); + + it('should log warning for invalid properties to auctionOptions', function () { + setConfig({ auctionOptions: { + 'testing': true + }}); + expect(logWarnSpy.calledOnce).to.equal(true); + const warning = 'Auction Options given an incorrect param: testing'; + assert.ok(logWarnSpy.calledWith(warning), 'expected warning was logged'); + }); }); diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index d30659791ea..edc7b7a2767 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -7,8 +7,8 @@ import { spec } from 'modules/33acrossBidAdapter.js'; describe('33acrossBidAdapter:', function () { const BIDDER_CODE = '33across'; - const SITE_ID = 'pub1234'; - const PRODUCT_ID = 'product1'; + const SITE_ID = 'sample33xGUID123456789'; + const PRODUCT_ID = 'siab'; const END_POINT = 'https://ssc.33across.com/api/v1/hb'; let element, win; @@ -17,39 +17,13 @@ describe('33acrossBidAdapter:', function () { function TtxRequestBuilder() { const ttxRequest = { - imp: [{ - banner: { - format: [ - { - w: 300, - h: 250 - }, - { - w: 728, - h: 90 - } - ], - ext: { - ttx: { - viewability: { - amount: 100 - } - } - } - }, - ext: { - ttx: { - prod: PRODUCT_ID - } - } - }], + imp: [{}], site: { id: SITE_ID }, id: 'b1', user: { ext: { - consent: undefined } }, regs: { @@ -69,13 +43,52 @@ describe('33acrossBidAdapter:', function () { } }; - this.withSizes = sizes => { + this.withBanner = () => { + Object.assign(ttxRequest.imp[0], { + banner: { + format: [ + { + w: 300, + h: 250 + }, + { + w: 728, + h: 90 + } + ], + ext: { + ttx: { + viewability: { + amount: 100 + } + } + } + } + }); + + return this; + }; + + this.withBannerSizes = this.withSizes = sizes => { Object.assign(ttxRequest.imp[0].banner, { format: sizes }); return this; }; - this.withViewability = viewability => { - Object.assign(ttxRequest.imp[0].banner, { + this.withVideo = (params = {}) => { + Object.assign(ttxRequest.imp[0], { + video: { + w: 300, + h: 250, + placement: 2, + ...params + } + }); + + return this; + }; + + this.withViewability = (viewability, format = 'banner') => { + Object.assign(ttxRequest.imp[0][format], { ext: { ttx: { viewability } } @@ -83,6 +96,18 @@ describe('33acrossBidAdapter:', function () { return this; }; + this.withProduct = (prod = PRODUCT_ID) => { + Object.assign(ttxRequest.imp[0], { + ext: { + ttx: { + prod + } + } + }); + + return this; + }; + this.withGdprConsent = (consent, gdpr) => { Object.assign(ttxRequest, { user: { @@ -140,18 +165,31 @@ describe('33acrossBidAdapter:', function () { return this; }; - this.withFormatFloors = floors => { - const format = ttxRequest.imp[0].banner.format.map((fm, i) => { - return Object.assign(fm, { - ext: { - ttx: { - bidfloors: [ floors[i] ] + this.withFloors = this.withFormatFloors = (mediaType, floors) => { + switch (mediaType) { + case 'banner': + const format = ttxRequest.imp[0].banner.format.map((fm, i) => { + return Object.assign(fm, { + ext: { + ttx: { + bidfloors: [ floors[i] ] + } + } + }) + }); + + ttxRequest.imp[0].banner.format = format; + break; + case 'video': + Object.assign(ttxRequest.imp[0].video, { + ext: { + ttx: { + bidfloors: floors + } } - } - }) - }); - - ttxRequest.imp[0].banner.format = format; + }); + break; + } return this; }; @@ -188,6 +226,53 @@ describe('33acrossBidAdapter:', function () { this.build = () => serverRequest; } + function BidRequestsBuilder() { + const bidRequests = [ + { + bidId: 'b1', + bidder: '33across', + bidderRequestId: 'b1a', + params: { + siteId: SITE_ID, + productId: PRODUCT_ID + }, + adUnitCode: 'div-id', + auctionId: 'r1', + mediaTypes: {}, + transactionId: 't1' + } + ]; + + this.withBanner = () => { + bidRequests[0].mediaTypes.banner = { + sizes: [ + [300, 250], + [728, 90] + ] + }; + + return this; + }; + + this.withProduct = (prod) => { + bidRequests[0].params.productId = prod; + + return this; + }; + + this.withVideo = (params) => { + bidRequests[0].mediaTypes.video = { + playerSize: [[300, 250]], + context: 'outstream', + ...params + }; + + return this; + } + + this.build = () => bidRequests; + } + beforeEach(function() { element = { x: 0, @@ -217,24 +302,11 @@ describe('33acrossBidAdapter:', function () { innerHeight: 600 }; - bidRequests = [ - { - bidId: 'b1', - bidder: '33across', - bidderRequestId: 'b1a', - params: { - siteId: SITE_ID, - productId: PRODUCT_ID - }, - adUnitCode: 'div-id', - auctionId: 'r1', - sizes: [ - [300, 250], - [728, 90] - ], - transactionId: 't1' - } - ]; + bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .build() + ); sandbox = sinon.sandbox.create(); sandbox.stub(Date, 'now').returns(1); @@ -248,78 +320,246 @@ describe('33acrossBidAdapter:', function () { }); describe('isBidRequestValid:', function() { - it('returns true when valid bid request is sent', function() { - const validBid = { - bidder: BIDDER_CODE, - params: { - siteId: SITE_ID, - productId: PRODUCT_ID - } - }; + context('basic validation', function() { + it('returns true for valid guid values', function() { + // NOTE: We ignore whitespace at the start and end since + // in our experience these are common typos + const validGUIDs = [ + `${SITE_ID}`, + `${SITE_ID} `, + ` ${SITE_ID}`, + ` ${SITE_ID} ` + ]; - expect(spec.isBidRequestValid(validBid)).to.be.true; - }); + validGUIDs.forEach((siteId) => { + const bid = { + bidder: '33across', + params: { + siteId + } + }; - it('returns true when valid test bid request is sent', function() { - const validBid = { - bidder: BIDDER_CODE, - params: { - siteId: SITE_ID, - productId: PRODUCT_ID, - test: 1 - } - }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + }); - expect(spec.isBidRequestValid(validBid)).to.be.true; - }); + it('returns false for invalid guid values', function() { + const invalidGUIDs = [ + undefined, + 'siab' + ]; - it('returns false when bidder not set to "33across"', function() { - const invalidBid = { - bidder: 'foo', - params: { - siteId: SITE_ID, - productId: PRODUCT_ID - } - }; + invalidGUIDs.forEach((siteId) => { + const bid = { + bidder: '33across', + params: { + siteId + } + }; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); }); - it('returns false when params not set', function() { - const invalidBid = { - bidder: 'foo' - }; + context('banner validation', function() { + it('returns true when banner mediaType does not exist', function() { + const bid = { + bidder: '33across', + params: { + siteId: 'cxBE0qjUir6iopaKkGJozW' + } + }; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; - }); + expect(spec.isBidRequestValid(bid)).to.be.true; + }); - it('returns false when site ID is not set in params', function() { - const invalidBid = { - bidder: 'foo', - params: { - productId: PRODUCT_ID - } - }; + it('returns true when banner sizes are defined', function() { + const bid = { + bidder: '33across', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + siteId: 'cxBE0qjUir6iopaKkGJozW' + } + }; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; - }); + expect(spec.isBidRequestValid(bid)).to.be.true; + }); - it('returns false when product ID not set in params', function() { - const invalidBid = { - bidder: 'foo', - params: { - siteId: SITE_ID - } - }; + it('returns false when banner sizes are invalid', function() { + const invalidSizes = [ + undefined, + '16:9', + 300, + 'foo' + ]; + + invalidSizes.forEach((sizes) => { + const bid = { + bidder: '33across', + mediaTypes: { + banner: { + sizes + } + }, + params: { + siteId: 'cxBE0qjUir6iopaKkGJozW' + } + }; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); }); + + context('video validation', function() { + beforeEach(function() { + // Basic Valid BidRequest + this.bid = { + bidder: '33across', + mediaTypes: { + video: { + playerSize: [[300, 50]], + context: 'outstream', + mimes: ['foo', 'bar'], + protocols: [1, 2] + } + }, + params: { + siteId: `${SITE_ID}` + } + }; + }); + + it('returns true when video mediaType does not exist', function() { + const bid = { + bidder: '33across', + params: { + siteId: `${SITE_ID}` + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('returns true when valid video mediaType is defined', function() { + expect(spec.isBidRequestValid(this.bid)).to.be.true; + }); + + it('returns false when video context is not defined', function() { + delete this.bid.mediaTypes.video.context; + + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + + it('returns false when video playserSize is invalid', function() { + const invalidSizes = [ + undefined, + '16:9', + 300, + 'foo' + ]; + + invalidSizes.forEach((playerSize) => { + this.bid.mediaTypes.video.playerSize = playerSize; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video mimes is invalid', function() { + const invalidMimes = [ + undefined, + 'foo', + 1, + [] + ] + + invalidMimes.forEach((mimes) => { + this.bid.mediaTypes.video.mimes = mimes; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video protocols is invalid', function() { + const invalidMimes = [ + undefined, + 'foo', + 1, + [] + ] + + invalidMimes.forEach((protocols) => { + this.bid.mediaTypes.video.protocols = protocols; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video placement is invalid', function() { + const invalidPlacement = [ + [], + '1', + {}, + 'foo' + ]; + + invalidPlacement.forEach((placement) => { + this.bid.mediaTypes.video.placement = placement; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video startdelay is invalid for instream context', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'instream', protocols: [1, 2], mimes: ['foo', 'bar']}) + .build() + ); + + const invalidStartdelay = [ + [], + '1', + {}, + 'foo' + ]; + + invalidStartdelay.forEach((startdelay) => { + bidRequests[0].mediaTypes.video.startdelay = startdelay; + expect(spec.isBidRequestValid(bidRequests[0])).to.be.false; + }); + }); + + it('returns true when video startdelay is invalid for outstream context', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream', protocols: [1, 2], mimes: ['foo', 'bar']}) + .build() + ); + + const invalidStartdelay = [ + [], + '1', + {}, + 'foo' + ]; + + invalidStartdelay.forEach((startdelay) => { + bidRequests[0].mediaTypes.video.startdelay = startdelay; + expect(spec.isBidRequestValid(bidRequests[0])).to.be.true; + }); + }); + }) }); describe('buildRequests:', function() { context('when element is fully in view', function() { it('returns 100', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 100}) .build(); const serverRequest = new ServerRequestBuilder() @@ -335,6 +575,8 @@ describe('33acrossBidAdapter:', function () { context('when element is out of view', function() { it('returns 0', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 0}) .build(); const serverRequest = new ServerRequestBuilder() @@ -350,6 +592,8 @@ describe('33acrossBidAdapter:', function () { context('when element is partially in view', function() { it('returns percentage', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 75}) .build(); const serverRequest = new ServerRequestBuilder() @@ -365,6 +609,8 @@ describe('33acrossBidAdapter:', function () { context('when width or height of the element is zero', function() { it('try to use alternative values', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withSizes([{ w: 800, h: 2400 }]) .withViewability({amount: 25}) .build(); @@ -373,7 +619,7 @@ describe('33acrossBidAdapter:', function () { .build(); Object.assign(element, { width: 0, height: 0 }); - bidRequests[0].sizes = [[800, 2400]]; + bidRequests[0].mediaTypes.banner.sizes = [[800, 2400]]; expect(spec.buildRequests(bidRequests)).to.deep.equal([ serverRequest ]); }); @@ -382,6 +628,8 @@ describe('33acrossBidAdapter:', function () { context('when nested iframes', function() { it('returns \'nm\'', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: spec.NON_MEASURABLE}) .build(); const serverRequest = new ServerRequestBuilder() @@ -402,6 +650,8 @@ describe('33acrossBidAdapter:', function () { context('when tab is inactive', function() { it('returns 0', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 0}) .build(); const serverRequest = new ServerRequestBuilder() @@ -432,6 +682,8 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with gdpr consent data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withGdprConsent('foobarMyPreference', 1) .build(); const serverRequest = new ServerRequestBuilder() @@ -450,6 +702,8 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withGdprConsent('foobarMyPreference', 1) .build(); const serverRequest = new ServerRequestBuilder() @@ -471,6 +725,8 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with default gdpr consent data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -488,6 +744,8 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -510,6 +768,8 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with us_privacy consent data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withUspConsent('foo') .build(); const serverRequest = new ServerRequestBuilder() @@ -528,6 +788,8 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withUspConsent('foo') .build(); const serverRequest = new ServerRequestBuilder() @@ -549,6 +811,8 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with default us_privacy data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -566,6 +830,8 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -586,6 +852,8 @@ describe('33acrossBidAdapter:', function () { }; const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withPageUrl('http://foo.com/bar') .build(); const serverRequest = new ServerRequestBuilder() @@ -605,6 +873,8 @@ describe('33acrossBidAdapter:', function () { }; const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -656,6 +926,8 @@ describe('33acrossBidAdapter:', function () { bidRequests[0].schain = schain; const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withSchain(schain) .build(); const serverRequest = new ServerRequestBuilder() @@ -672,6 +944,8 @@ describe('33acrossBidAdapter:', function () { context('when there no schain object is passed', function() { it('does not set source field', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() @@ -684,9 +958,11 @@ describe('33acrossBidAdapter:', function () { }); }); - context('when price floor module is not enabled in bidRequest', function() { + context('when price floor module is not enabled for banner in bidRequest', function() { it('does not set any bidfloors in ttxRequest', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -697,11 +973,13 @@ describe('33acrossBidAdapter:', function () { }); }); - context('when price floor module is enabled in bidRequest', function() { + context('when price floor module is enabled for banner in bidRequest', function() { it('does not set any bidfloors in ttxRequest if there is no floor', function() { bidRequests[0].getFloor = () => ({}); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -723,9 +1001,188 @@ describe('33acrossBidAdapter:', function () { }; const ttxRequest = new TtxRequestBuilder() - .withFormatFloors([ 1.0, 0.10 ]) + .withBanner() + .withProduct() + .withFormatFloors('banner', [ 1.0, 0.10 ]) + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + }); + + context('when mediaType has video only and context is instream', function() { + it('builds instream request with default params', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'instream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct('instream') + .build(); + + ttxRequest.imp[0].video.placement = 1; + ttxRequest.imp[0].video.startdelay = 0; + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + + it('builds instream request with params passed', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'instream', startdelay: -2}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo({startdelay: -2, placement: 1}) + .withProduct('instream') + .build(); + + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(JSON.parse(builtServerRequests[0].data)).to.deep.equal(ttxRequest); + }); + }); + + context('when mediaType has video only and context is outstream', function() { + it('builds siab request with video only with default params', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct('siab') + .build(); + + ttxRequest.imp[0].video.placement = 2; + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + + it('builds siab request with video params passed', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream', placement: 3, playbackmethod: [2]}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo({placement: 3, playbackmethod: [2]}) + .withProduct('siab') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + }); + + context('when mediaType has banner only', function() { + it('builds default siab request', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct('siab') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + + it('builds default inview request when product is set as such', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .withProduct('inview') + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct('inview') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + }); + + context('when mediaType has banner and video', function() { + it('builds siab request with banner and outstream video', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .withVideo({context: 'outstream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withVideo() + .withProduct('siab') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + + it('builds siab request with banner and outstream video even when context is instream', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .withVideo({context: 'instream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withVideo() + .withProduct('siab') .build(); + ttxRequest.imp[0].video.placement = 2; + const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); @@ -734,6 +1191,55 @@ describe('33acrossBidAdapter:', function () { expect(builtServerRequests).to.deep.equal([serverRequest]); }); }); + + context('when price floor module is enabled for video in bidRequest', function() { + it('does not set any bidfloors in video if there is no floor', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream'}) + .build() + ); + + bidRequests[0].getFloor = () => ({}); + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct() + .build(); + + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(JSON.parse(builtServerRequests[0].data)).to.deep.equal(ttxRequest); + }); + + it('sets bidfloors in video if there is a floor', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream'}) + .build() + ); + + bidRequests[0].getFloor = ({size, currency, mediaType}) => { + const floor = (mediaType === 'video') ? 1.0 : 0.10 + return ( + { + floor, + currency: 'USD' + } + ); + }; + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct() + .withFloors('video', [ 1.0 ]) + .build(); + + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(JSON.parse(builtServerRequests[0].data)).to.deep.equal(ttxRequest); + }); + }); }); describe('interpretResponse', function() { @@ -741,6 +1247,8 @@ describe('33acrossBidAdapter:', function () { beforeEach(function() { ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withSite({ id: SITE_ID, page: 'https://test-url.com' @@ -757,7 +1265,7 @@ describe('33acrossBidAdapter:', function () { }); context('when exactly one bid is returned', function() { - it('interprets and returns the single bid response', function() { + it('interprets and returns the single banner bid response', function() { const serverResponse = { cur: 'USD', ext: {}, @@ -784,12 +1292,56 @@ describe('33acrossBidAdapter:', function () { ad: '

I am an ad

', ttl: 60, creativeId: 1, + mediaType: 'banner', currency: 'USD', netRevenue: true }; expect(spec.interpretResponse({ body: serverResponse }, serverRequest)).to.deep.equal([bidResponse]); }); + + it('interprets and returns the single video bid response', function() { + const videoBid = ''; + const serverResponse = { + cur: 'USD', + ext: {}, + id: 'b1', + seatbid: [ + { + bid: [{ + id: '1', + adm: videoBid, + ext: { + ttx: { + mediaType: 'video', + vastType: 'xml' + } + }, + crid: 1, + h: 250, + w: 300, + price: 0.0938 + }] + } + ] + }; + const bidResponse = { + requestId: 'b1', + bidderCode: BIDDER_CODE, + cpm: 0.0938, + width: 300, + height: 250, + ad: videoBid, + ttl: 60, + creativeId: 1, + mediaType: 'video', + currency: 'USD', + netRevenue: true, + vastXml: videoBid + }; + + expect(spec.interpretResponse({ body: serverResponse }, serverRequest)).to.deep.equal([bidResponse]); + }); }); context('when no bids are returned', function() { @@ -852,6 +1404,7 @@ describe('33acrossBidAdapter:', function () { ad: '

I am an ad

', ttl: 60, creativeId: 1, + mediaType: 'banner', currency: 'USD', netRevenue: true }; @@ -886,9 +1439,13 @@ describe('33acrossBidAdapter:', function () { }, adUnitCode: 'div-id', auctionId: 'r1', - sizes: [ - [300, 250] - ], + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, transactionId: 't1' }, { @@ -901,9 +1458,13 @@ describe('33acrossBidAdapter:', function () { }, adUnitCode: 'div-id', auctionId: 'r1', - sizes: [ - [300, 250] - ], + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, transactionId: 't2' } ]; diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index a18cd797d68..2cf97a1129b 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -1,6 +1,16 @@ import find from 'core-js-pure/features/array/find.js'; import { expect } from 'chai'; -import { _features, internal as adagio, adagioScriptFromLocalStorageCb, getAdagioScript, storage, spec, ENDPOINT, VERSION } from '../../../modules/adagioBidAdapter.js'; +import { + _features, + internal as adagio, + adagioScriptFromLocalStorageCb, + getAdagioScript, + storage, + spec, + ENDPOINT, + VERSION, + RENDERER_URL +} from '../../../modules/adagioBidAdapter.js'; import { loadExternalScript } from '../../../src/adloader.js'; import * as utils from '../../../src/utils.js'; import { config } from 'src/config.js'; @@ -107,7 +117,7 @@ describe('Adagio bid adapter', () => { adagioMock = sinon.mock(adagio); utilsMock = sinon.mock(utils); - sandbox = sinon.sandbox.create(); + sandbox = sinon.createSandbox(); }); afterEach(() => { @@ -254,7 +264,7 @@ describe('Adagio bid adapter', () => { // replace by the values defined in beforeEach window.top.ADAGIO = { ...window.ADAGIO - } + }; spec.isBidRequestValid(bid01); spec.isBidRequestValid(bid02); @@ -287,6 +297,7 @@ describe('Adagio bid adapter', () => { 'pageviewId', 'adUnits', 'regs', + 'user', 'schain', 'prebidVersion', 'adapterVersion', @@ -383,6 +394,81 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.adUnits[0].features.url).to.not.exist; }); + describe('With video mediatype', function() { + context('Outstream video', function() { + it('should logWarn if user does not set renderer.backupOnly: true', function() { + sandbox.spy(utils, 'logWarn'); + const bid01 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-01', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + renderer: { + url: 'https://url.tld', + render: () => true + } + } + }, + }).withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + const request = spec.buildRequests([bid01], bidderRequest)[0]; + + expect(request.data.adUnits[0].mediaTypes.video.playerName).to.equal('other'); + sinon.assert.calledWith(utils.logWarn, 'Adagio: renderer.backupOnly has not been set. Adagio recommends to use its own player to get expected behavior.'); + }); + }); + + it('Update mediaTypes.video with OpenRTB options. Validate and sanitize whitelisted OpenRTB', function() { + sandbox.spy(utils, 'logWarn'); + const bid01 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-01', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + mimes: ['video/mp4'], + api: 5, // will be removed because invalid + playbackmethod: [7], // will be removed because invalid + } + }, + }).withParams({ + // options in video, will overide + video: { + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30, + placement: [3], + protocols: [8] + } + }).build(); + + const bidderRequest = new BidderRequestBuilder().build(); + const expected = { + context: 'outstream', + playerSize: [[300, 250]], + playerName: 'adagio', + mimes: ['video/mp4'], + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30, + placement: [3], + protocols: [8], + w: 300, + h: 250 + }; + + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests).to.have.lengthOf(1); + expect(requests[0].data.adUnits[0].mediaTypes.video).to.deep.equal(expected); + sinon.assert.calledTwice(utils.logWarn); + }); + }); + describe('with sChain', function() { const schain = { ver: '1.0', @@ -549,7 +635,7 @@ describe('Adagio bid adapter', () => { describe('with USPrivacy', function() { const bid01 = new BidRequestBuilder().withParams().build(); - const consent = 'Y11N' + const consent = 'Y11N'; it('should send the USPrivacy "ccpa.uspConsent" in the request', function () { const bidderRequest = new BidderRequestBuilder({ @@ -573,6 +659,53 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.regs.ccpa).to.be.empty; }); }); + + describe('with userID modules', function() { + const userId = { + sharedid: {id: '01EAJWWNEPN3CYMM5N8M5VXY22', third: '01EAJWWNEPN3CYMM5N8M5VXY22'}, + unsuported: '666' + }; + + it('should send "user.eids" in the request for Prebid.js supported modules only', function() { + const bid01 = new BidRequestBuilder({ + userId + }).withParams().build(); + + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + const expected = [{ + source: 'sharedid.org', + uids: [ + { + atype: 1, + ext: { + third: '01EAJWWNEPN3CYMM5N8M5VXY22' + }, + id: '01EAJWWNEPN3CYMM5N8M5VXY22' + } + ] + }]; + + expect(requests[0].data.user.eids).to.have.lengthOf(1); + expect(requests[0].data.user.eids).to.deep.equal(expected); + }); + + it('should send an empty "user.eids" array in the request if userId module is unsupported', function() { + const bid01 = new BidRequestBuilder({ + userId: { + unsuported: '666' + } + }).withParams().build(); + + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.user.eids).to.be.empty; + }); + }); }); describe('interpretResponse()', function() { @@ -684,6 +817,37 @@ describe('Adagio bid adapter', () => { utilsMock.verify(); }); + + describe('Response with video outstream', () => { + const bidRequestWithOutstream = utils.deepClone(bidRequest); + bidRequestWithOutstream.data.adUnits[0].mediaTypes.video = { + context: 'outstream', + playerSize: [[300, 250]], + mimes: ['video/mp4'], + skip: true + }; + + const serverResponseWithOutstream = utils.deepClone(serverResponse); + serverResponseWithOutstream.body.bids[0].vastXml = ''; + serverResponseWithOutstream.body.bids[0].mediaType = 'video'; + serverResponseWithOutstream.body.bids[0].outstream = { + bvwUrl: 'https://foo.baz', + impUrl: 'https://foo.bar' + }; + + it('should set a renderer in video outstream context', function() { + const bidResponse = spec.interpretResponse(serverResponseWithOutstream, bidRequestWithOutstream)[0]; + expect(bidResponse).to.have.any.keys('outstream', 'renderer', 'mediaType'); + expect(bidResponse.renderer).to.be.a('object'); + expect(bidResponse.renderer.url).to.equal(RENDERER_URL); + expect(bidResponse.renderer.config.bvwUrl).to.be.ok; + expect(bidResponse.renderer.config.impUrl).to.be.ok; + expect(bidResponse.renderer.loaded).to.not.be.ok; + expect(bidResponse.width).to.equal(300); + expect(bidResponse.height).to.equal(250); + expect(bidResponse.vastUrl).to.match(/^data:text\/xml;/) + }); + }); }); describe('getUserSyncs()', function() { @@ -1147,7 +1311,7 @@ describe('Adagio bid adapter', () => { expect(loadExternalScript.called).to.be.false; expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null; - }) + }); }); it('should verify valid hash with valid script', function () { diff --git a/test/spec/modules/adformBidAdapter_spec.js b/test/spec/modules/adformBidAdapter_spec.js index 360979659de..23db7a8dc97 100644 --- a/test/spec/modules/adformBidAdapter_spec.js +++ b/test/spec/modules/adformBidAdapter_spec.js @@ -149,6 +149,14 @@ describe('Adform adapter', function () { }); }); + it('should allow to pass custom extended ids', function () { + bids[0].params.eids = 'some_id_value'; + let request = spec.buildRequests(bids); + let eids = parseUrl(request.url).query.eids; + + assert.equal(eids, 'some_id_value'); + }); + describe('user privacy', function () { it('should send GDPR Consent data to adform if gdprApplies', function () { let request = spec.buildRequests([bids[0]], {gdprConsent: {gdprApplies: true, consentString: 'concentDataString'}}); diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 87504aa46af..4d3dca7f344 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -175,7 +175,6 @@ describe('Adkernel adapter', function () { dealid: 'deal' }] }], - cur: 'USD', ext: { adk_usersync: [{type: 1, url: 'https://adk.sync.com/sync'}] } @@ -192,7 +191,6 @@ describe('Adkernel adapter', function () { cid: '16855' }] }], - cur: 'USD' }, usersyncOnlyResponse = { id: 'nobid1', ext: { @@ -222,12 +220,18 @@ describe('Adkernel adapter', function () { } }), adomain: ['displayurl.com'], + cat: ['IAB1-4', 'IAB8-16', 'IAB25-5'], cid: '1', - crid: '4' + crid: '4', + ext: { + 'advertiser_id': 777, + 'advertiser_name': 'advertiser', + 'agency_name': 'agency' + } }] }], bidid: 'pTuOlf5KHUo', - cur: 'USD' + cur: 'EUR' }; var sandbox; @@ -552,8 +556,7 @@ describe('Adkernel adapter', function () { describe('adapter configuration', () => { it('should have aliases', () => { - expect(spec.aliases).to.have.lengthOf(6); - expect(spec.aliases).to.include.members(['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon']); + expect(spec.aliases).to.have.lengthOf(9); }); }); @@ -587,7 +590,13 @@ describe('Adkernel adapter', function () { let resp = spec.interpretResponse({body: nativeResponse}, pbRequests[0])[0]; expect(resp).to.have.property('requestId', 'Bid_01'); expect(resp).to.have.property('cpm', 2.25); - expect(resp).to.have.property('currency', 'USD'); + expect(resp).to.have.property('currency', 'EUR'); + expect(resp).to.have.property('meta'); + expect(resp.meta.advertiserId).to.be.eql(777); + expect(resp.meta.advertiserName).to.be.eql('advertiser'); + expect(resp.meta.agencyName).to.be.eql('agency'); + expect(resp.meta.advertiserDomains).to.be.eql(['displayurl.com']); + expect(resp.meta.secondaryCatIds).to.be.eql(['IAB1-4', 'IAB8-16', 'IAB25-5']); expect(resp).to.have.property('mediaType', NATIVE); expect(resp).to.have.property('native'); expect(resp.native).to.have.property('clickUrl', 'http://rtb.com/click?i=pTuOlf5KHUo_0'); diff --git a/test/spec/modules/admixerBidAdapter_spec.js b/test/spec/modules/admixerBidAdapter_spec.js index 6d2e3059dc8..dfadf1f95d5 100644 --- a/test/spec/modules/admixerBidAdapter_spec.js +++ b/test/spec/modules/admixerBidAdapter_spec.js @@ -3,7 +3,7 @@ import {spec} from 'modules/admixerBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; const BIDDER_CODE = 'admixer'; -const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.0.aspx'; +const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.1.aspx'; const ZONE_ID = '2eb6bd58-865c-47ce-af7f-a918108c3fd2'; describe('AdmixerAdapter', function () { @@ -78,33 +78,37 @@ describe('AdmixerAdapter', function () { describe('interpretResponse', function () { let response = { - body: [{ - 'currency': 'USD', - 'cpm': 6.210000, - 'ad': '
ad
', - 'width': 300, - 'height': 600, - 'creativeId': 'ccca3e5e-0c54-4761-9667-771322fbdffc', - 'ttl': 360, - 'netRevenue': false, - 'bidId': '5e4e763b6bc60b' - }] + body: { + ads: [{ + 'currency': 'USD', + 'cpm': 6.210000, + 'ad': '
ad
', + 'width': 300, + 'height': 600, + 'creativeId': 'ccca3e5e-0c54-4761-9667-771322fbdffc', + 'ttl': 360, + 'netRevenue': false, + 'bidId': '5e4e763b6bc60b', + 'dealId': 'asd123', + }] + } }; it('should get correct bid response', function () { - const body = response.body; + const ads = response.body.ads; let expectedResponse = [ { - 'requestId': body[0].bidId, - 'cpm': body[0].cpm, - 'creativeId': body[0].creativeId, - 'width': body[0].width, - 'height': body[0].height, - 'ad': body[0].ad, + 'requestId': ads[0].bidId, + 'cpm': ads[0].cpm, + 'creativeId': ads[0].creativeId, + 'width': ads[0].width, + 'height': ads[0].height, + 'ad': ads[0].ad, 'vastUrl': undefined, - 'currency': body[0].currency, - 'netRevenue': body[0].netRevenue, - 'ttl': body[0].ttl, + 'currency': ads[0].currency, + 'netRevenue': ads[0].netRevenue, + 'ttl': ads[0].ttl, + 'dealId': ads[0].dealId, } ]; @@ -119,4 +123,34 @@ describe('AdmixerAdapter', function () { expect(result.length).to.equal(0); }); }); + + describe('getUserSyncs', function () { + let imgUrl = 'https://example.com/img1'; + let frmUrl = 'https://example.com/frm2'; + let responses = [{ + body: { + cm: { + pixels: [ + imgUrl + ], + iframes: [ + frmUrl + ], + } + } + }]; + + it('Returns valid values', function () { + let userSyncAll = spec.getUserSyncs({pixelEnabled: true, iframeEnabled: true}, responses); + let userSyncImg = spec.getUserSyncs({pixelEnabled: true, iframeEnabled: false}, responses); + let userSyncFrm = spec.getUserSyncs({pixelEnabled: false, iframeEnabled: true}, responses); + expect(userSyncAll).to.be.an('array').with.lengthOf(2); + expect(userSyncImg).to.be.an('array').with.lengthOf(1); + expect(userSyncImg[0].url).to.be.equal(imgUrl); + expect(userSyncImg[0].type).to.be.equal('image'); + expect(userSyncFrm).to.be.an('array').with.lengthOf(1); + expect(userSyncFrm[0].url).to.be.equal(frmUrl); + expect(userSyncFrm[0].type).to.be.equal('iframe'); + }); + }); }); diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js index 91315da8801..766045b0f3e 100644 --- a/test/spec/modules/amxBidAdapter_spec.js +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -189,6 +189,45 @@ describe('AmxBidAdapter', () => { expect(data.tm).to.equal(true); }); + it('if prebid is in an iframe, will use the frame url as domain, if the topmost is not avialable', () => { + const { data } = spec.buildRequests([sampleBidRequestBase], { + ...sampleBidderRequest, + refererInfo: { + numIframes: 1, + referer: 'http://search-traffic-source.com', + stack: [] + } + }); + expect(data.do).to.equal('localhost') + expect(data.re).to.equal('http://search-traffic-source.com'); + }); + + it('if we are in AMP, make sure we use the canonical URL or the referrer (which is sourceUrl)', () => { + const { data } = spec.buildRequests([sampleBidRequestBase], { + ...sampleBidderRequest, + refererInfo: { + isAmp: true, + referer: 'http://real-publisher-site.com/content', + stack: [] + } + }); + expect(data.do).to.equal('real-publisher-site.com') + expect(data.re).to.equal('http://real-publisher-site.com/content'); + }) + + it('if prebid is in an iframe, will use the topmost url as domain', () => { + const { data } = spec.buildRequests([sampleBidRequestBase], { + ...sampleBidderRequest, + refererInfo: { + numIframes: 1, + referer: 'http://search-traffic-source.com', + stack: ['http://top-site.com', 'http://iframe.com'] + } + }); + expect(data.do).to.equal('top-site.com'); + expect(data.re).to.equal('http://search-traffic-source.com'); + }); + it('handles referer data and GDPR, USP Consent, COPPA', () => { const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); delete data.m; // don't deal with "m" in this test diff --git a/test/spec/modules/aolBidAdapter_spec.js b/test/spec/modules/aolBidAdapter_spec.js index dd10a57bbfe..11e1a317b70 100644 --- a/test/spec/modules/aolBidAdapter_spec.js +++ b/test/spec/modules/aolBidAdapter_spec.js @@ -1,7 +1,7 @@ import {expect} from 'chai'; import * as utils from 'src/utils.js'; import {spec} from 'modules/aolBidAdapter.js'; -import {config} from 'src/config.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; const DEFAULT_AD_CONTENT = ''; @@ -80,6 +80,33 @@ describe('AolAdapter', function () { const NEXAGE_URL = 'https://c2shb.ssp.yahoo.com/bidRequest?'; const ONE_DISPLAY_TTL = 60; const ONE_MOBILE_TTL = 3600; + const SUPPORTED_USER_ID_SOURCES = { + 'adserver.org': '100', + 'criteo.com': '200', + 'id5-sync.com': '300', + 'intentiq.com': '400', + 'liveintent.com': '500', + 'quantcast.com': '600', + 'verizonmedia.com': '700', + 'liveramp.com': '800' + }; + + const USER_ID_DATA = { + criteoId: SUPPORTED_USER_ID_SOURCES['criteo.com'], + vmuid: SUPPORTED_USER_ID_SOURCES['verizonmedia.com'], + idl_env: SUPPORTED_USER_ID_SOURCES['liveramp.com'], + lipb: { + lipbid: SUPPORTED_USER_ID_SOURCES['liveintent.com'], + segments: ['100', '200'] + }, + tdid: SUPPORTED_USER_ID_SOURCES['adserver.org'], + id5id: { + uid: SUPPORTED_USER_ID_SOURCES['id5-sync.com'], + ext: {foo: 'bar'} + }, + intentIqId: SUPPORTED_USER_ID_SOURCES['intentiq.com'], + quantcastId: SUPPORTED_USER_ID_SOURCES['quantcast.com'] + }; function createCustomBidRequest({bids, params} = {}) { var bidderRequest = getDefaultBidRequest(); @@ -463,6 +490,18 @@ describe('AolAdapter', function () { '¶m1=val1¶m2=val2¶m3=val3¶m4=val4'); }); + Object.keys(SUPPORTED_USER_ID_SOURCES).forEach(source => { + it(`should set the user ID query param for ${source}`, function () { + let bidRequest = createCustomBidRequest({ + params: getNexageGetBidParams() + }); + bidRequest.bids[0].userId = {}; + bidRequest.bids[0].userIdAsEids = createEidsArray(USER_ID_DATA); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(`&eid${source}=${encodeURIComponent(SUPPORTED_USER_ID_SOURCES[source])}`); + }); + }); + it('should return request object for One Mobile POST endpoint when POST configuration is present', function () { let bidConfig = getNexagePostBidParams(); let bidRequest = createCustomBidRequest({ @@ -581,6 +620,22 @@ describe('AolAdapter', function () { } }); }); + + it('returns the bid object with eid array populated with PB set eids', () => { + let userIdBid = Object.assign({ + userId: {} + }, bid); + userIdBid.userIdAsEids = createEidsArray(USER_ID_DATA); + expect(spec.buildOpenRtbRequestData(userIdBid)).to.deep.equal({ + id: 'bid-id', + imp: [], + user: { + ext: { + eids: userIdBid.userIdAsEids + } + } + }); + }); }); describe('getUserSyncs()', function () { diff --git a/test/spec/modules/quantumdexBidAdapter_spec.js b/test/spec/modules/apacdexBidAdapter_spec.js similarity index 92% rename from test/spec/modules/quantumdexBidAdapter_spec.js rename to test/spec/modules/apacdexBidAdapter_spec.js index d1817493b36..da9a050a8de 100644 --- a/test/spec/modules/quantumdexBidAdapter_spec.js +++ b/test/spec/modules/apacdexBidAdapter_spec.js @@ -1,14 +1,14 @@ import { expect } from 'chai' -import { spec } from 'modules/quantumdexBidAdapter.js' +import { spec } from 'modules/apacdexBidAdapter.js' import { newBidder } from 'src/adapters/bidderFactory.js' import { userSync } from '../../../src/userSync.js'; -describe('QuantumdexBidAdapter', function () { +describe('ApacdexBidAdapter', function () { const adapter = newBidder(spec) describe('.code', function () { - it('should return a bidder code of quantumdex', function () { - expect(spec.code).to.equal('quantumdex') + it('should return a bidder code of apacdex', function () { + expect(spec.code).to.equal('apacdex') }) }) @@ -21,7 +21,7 @@ describe('QuantumdexBidAdapter', function () { describe('.isBidRequestValid', function () { it('should return false if there are no params', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', 'mediaTypes': { banner: { @@ -37,7 +37,7 @@ describe('QuantumdexBidAdapter', function () { it('should return false if there is no siteId param', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { site_id: '1a2b3c4d5e6f1a2b3c4d', @@ -56,7 +56,7 @@ describe('QuantumdexBidAdapter', function () { it('should return false if there is no mediaTypes', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -72,7 +72,7 @@ describe('QuantumdexBidAdapter', function () { it('should return true if the bid is valid', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -92,7 +92,7 @@ describe('QuantumdexBidAdapter', function () { describe('banner', () => { it('should return false if there are no banner sizes', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -111,7 +111,7 @@ describe('QuantumdexBidAdapter', function () { it('should return true if there is banner sizes', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -132,7 +132,7 @@ describe('QuantumdexBidAdapter', function () { describe('video', () => { it('should return false if there is no playerSize defined in the video mediaType', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d', @@ -152,7 +152,7 @@ describe('QuantumdexBidAdapter', function () { it('should return true if there is playerSize defined on the video mediaType', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d', @@ -196,7 +196,7 @@ describe('QuantumdexBidAdapter', function () { }, ] }, - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '1a2b3c4d5e6f1a2b3c4d', }, @@ -206,7 +206,7 @@ describe('QuantumdexBidAdapter', function () { 'bidId': '30b31c1838de1f', }, { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'ad_unit': '/7780971/sparks_prebid_LB', 'sizes': [[300, 250], [300, 600]], @@ -235,14 +235,14 @@ describe('QuantumdexBidAdapter', function () { it('should return a properly formatted request', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') expect(bidRequests.bidderRequests).to.eql(bidRequest); }) it('should return a properly formatted request with GDPR applies set to true', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr.gdprApplies).to.equal(true) expect(bidRequests.data.gdpr.consentString).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A==') @@ -251,7 +251,7 @@ describe('QuantumdexBidAdapter', function () { it('should return a properly formatted request with GDPR applies set to false', function () { bidderRequests.gdprConsent.gdprApplies = false; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr.gdprApplies).to.equal(false) expect(bidRequests.data.gdpr.consentString).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A==') @@ -271,7 +271,7 @@ describe('QuantumdexBidAdapter', function () { } }; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr.gdprApplies).to.equal(false) expect(bidRequests.data.gdpr).to.not.include.keys('consentString') @@ -291,7 +291,7 @@ describe('QuantumdexBidAdapter', function () { } }; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr.gdprApplies).to.equal(true) expect(bidRequests.data.gdpr).to.not.include.keys('consentString') @@ -309,7 +309,7 @@ describe('QuantumdexBidAdapter', function () { describe('.interpretResponse', function () { const bidRequests = { 'method': 'POST', - 'url': 'https://useast.quantumdex.io/auction/quantumdex', + 'url': 'https://useast.quantumdex.io/auction/apacdex', 'withCredentials': true, 'data': { 'device': { @@ -328,7 +328,7 @@ describe('QuantumdexBidAdapter', function () { }, 'bidderRequests': [ { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '343' }, @@ -363,7 +363,7 @@ describe('QuantumdexBidAdapter', function () { } }, { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '343' }, @@ -398,7 +398,7 @@ describe('QuantumdexBidAdapter', function () { } }, { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '343' }, @@ -464,12 +464,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.07, 'width': 160, 'height': 600, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -477,12 +477,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1, 'width': 300, 'height': 250, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -490,12 +490,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.25, 'width': 300, 'height': 250, - 'vastXml': 'quantumdex', + 'vastXml': 'apacdex', 'ttl': 500, 'creativeId': '30292e432662bd5f86d90774b944b038', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'video' } ], @@ -512,12 +512,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.07, 'width': 160, 'height': 600, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -525,12 +525,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1, 'width': 300, 'height': 250, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -538,12 +538,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.25, 'width': 300, 'height': 250, - 'vastXml': 'quantumdex', + 'vastXml': 'apacdex', 'ttl': 500, 'creativeId': '30292e432662bd5f86d90774b944b038', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'video' } ]; @@ -561,10 +561,10 @@ describe('QuantumdexBidAdapter', function () { expect(resp.currency).to.equal(prebidResponse[i].currency); expect(resp.dealId).to.equal(prebidResponse[i].dealId); if (resp.mediaType === 'video') { - expect(resp.vastXml.indexOf('quantumdex')).to.be.greaterThan(0); + expect(resp.vastXml.indexOf('apacdex')).to.be.greaterThan(0); } if (resp.mediaType === 'banner') { - expect(resp.ad.indexOf('Quantumdex AD')).to.be.greaterThan(0); + expect(resp.ad.indexOf('Apacdex AD')).to.be.greaterThan(0); } }); }); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 4102896ba94..426259639e8 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -808,7 +808,7 @@ describe('AppNexusAdapter', function () { expect(request.options).to.deep.equal({withCredentials: false}); }); - it('should populate eids and tpuids when ttd id and criteo is available', function () { + it('should populate eids when ttd id and criteo is available', function () { const bidRequest = Object.assign({}, bidRequests[0], { userId: { tdid: 'sample-userid', @@ -824,9 +824,9 @@ describe('AppNexusAdapter', function () { rti_partner: 'TDID' }); - expect(payload.tpuids).to.deep.include({ - provider: 'criteo', - user_id: 'sample-criteo-userid', + expect(payload.eids).to.deep.include({ + source: 'criteo.com', + id: 'sample-criteo-userid', }); }); diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index 5711e111e30..661780ffac0 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -223,17 +223,33 @@ describe('BeachfrontAdapter', function () { expect(data.imp[0].video).to.deep.contain({ w: width, h: height }); }); - it('must override video targeting params', function () { + it('must set video params from the standard object', function () { const bidRequest = bidRequests[0]; const mimes = ['video/webm']; const playbackmethod = 2; const maxduration = 30; const placement = 4; - bidRequest.mediaTypes = { video: {} }; - bidRequest.params.video = { mimes, playbackmethod, maxduration, placement }; + const skip = 1; + bidRequest.mediaTypes = { + video: { mimes, playbackmethod, maxduration, placement, skip } + }; const requests = spec.buildRequests([ bidRequest ]); const data = requests[0].data; - expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement }); + expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement, skip }); + }); + + it('must override video params from the bidder object', function () { + const bidRequest = bidRequests[0]; + const mimes = ['video/webm']; + const playbackmethod = 2; + const maxduration = 30; + const placement = 4; + const skip = 1; + bidRequest.mediaTypes = { video: { placement: 3, skip: 0 } }; + bidRequest.params.video = { mimes, playbackmethod, maxduration, placement, skip }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement, skip }); }); it('must add US privacy data to the request', function () { @@ -279,6 +295,24 @@ describe('BeachfrontAdapter', function () { }] }); }); + + it('must add the IdentityLink ID to the request', () => { + const idl_env = '4321'; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { video: {} }; + bidRequest.userId = { idl_env }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.user.ext.eids[0]).to.deep.equal({ + source: 'liveramp.com', + uids: [{ + id: idl_env, + ext: { + rtiPartner: 'idl' + } + }] + }); + }); }); describe('for banner bids', function () { @@ -419,6 +453,16 @@ describe('BeachfrontAdapter', function () { const data = requests[0].data; expect(data.tdid).to.equal(tdid); }); + + it('must add the IdentityLink ID to the request', () => { + const idl_env = '4321'; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + bidRequest.userId = { idl_env }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.idl).to.equal(idl_env); + }); }); describe('for multi-format bids', function () { @@ -486,16 +530,6 @@ describe('BeachfrontAdapter', function () { expect(bidResponse.length).to.equal(0); }); - it('should return no bids if the response "url" is missing', function () { - const bidRequest = bidRequests[0]; - bidRequest.mediaTypes = { video: {} }; - const serverResponse = { - bidPrice: 5.00 - }; - const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); - expect(bidResponse.length).to.equal(0); - }); - it('should return no bids if the response "bidPrice" is missing', function () { const bidRequest = bidRequests[0]; bidRequest.mediaTypes = { video: {} }; @@ -518,6 +552,7 @@ describe('BeachfrontAdapter', function () { const serverResponse = { bidPrice: 5.00, url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', + vast: '', crid: '123abc' }; const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); @@ -527,7 +562,7 @@ describe('BeachfrontAdapter', function () { cpm: serverResponse.bidPrice, creativeId: serverResponse.crid, vastUrl: serverResponse.url, - vastXml: undefined, + vastXml: serverResponse.vast, width: width, height: height, renderer: null, @@ -558,7 +593,7 @@ describe('BeachfrontAdapter', function () { }); }); - it('should return vast xml if found on the bid response', () => { + it('should return only vast url if the response type is "nurl"', () => { const width = 640; const height = 480; const bidRequest = bidRequests[0]; @@ -567,6 +602,7 @@ describe('BeachfrontAdapter', function () { playerSize: [ width, height ] } }; + bidRequest.params.video = { responseType: 'nurl' }; const serverResponse = { bidPrice: 5.00, url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', @@ -574,10 +610,29 @@ describe('BeachfrontAdapter', function () { crid: '123abc' }; const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); - expect(bidResponse).to.deep.contain({ - vastUrl: serverResponse.url, - vastXml: serverResponse.vast - }); + expect(bidResponse.vastUrl).to.equal(serverResponse.url); + expect(bidResponse.vastXml).to.equal(undefined); + }); + + it('should return only vast xml if the response type is "adm"', () => { + const width = 640; + const height = 480; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { + video: { + playerSize: [ width, height ] + } + }; + bidRequest.params.video = { responseType: 'adm' }; + const serverResponse = { + bidPrice: 5.00, + url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', + vast: '', + crid: '123abc' + }; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.vastUrl).to.equal(undefined); + expect(bidResponse.vastXml).to.equal(serverResponse.vast); }); it('should return a renderer for outstream video bids', function () { diff --git a/test/spec/modules/bizzclickBidAdapter_spec.js b/test/spec/modules/bizzclickBidAdapter_spec.js new file mode 100644 index 00000000000..39ad4ae39c9 --- /dev/null +++ b/test/spec/modules/bizzclickBidAdapter_spec.js @@ -0,0 +1,369 @@ +import { expect } from 'chai'; +import { spec } from 'modules/bizzclickBidAdapter.js'; + +const NATIVE_BID_REQUEST = { + code: 'native_example', + mediaTypes: { + native: { + title: { + required: true, + len: 800 + }, + image: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + privacyLink: { + required: false + }, + body: { + required: true + }, + icon: { + required: true, + sizes: [50, 50] + } + } + }, + bidder: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000 + +}; + +const BANNER_BID_REQUEST = { + code: 'banner_example', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidder: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000, + +} + +const bidRequest = { + refererInfo: { + referer: 'test.com' + } +} + +const VIDEO_BID_REQUEST = { + code: 'video1', + sizes: [640, 480], + mediaTypes: { video: { + minduration: 0, + maxduration: 999, + boxingallowed: 1, + skip: 0, + mimes: [ + 'application/javascript', + 'video/mp4' + ], + w: 1920, + h: 1080, + protocols: [ + 2 + ], + linearity: 1, + api: [ + 1, + 2 + ] + } + }, + + bidder: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000 + +} + +const BANNER_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: 'admcode', + crid: 'crid', + ext: { + mediaType: 'banner' + } + }], + }], +}; + +const VIDEO_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: 'admcode', + crid: 'crid', + ext: { + mediaType: 'video', + vastUrl: 'http://example.vast', + } + }], + }], +}; + +let imgData = { + url: `https://example.com/image`, + w: 1200, + h: 627 +}; + +const NATIVE_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: { native: + { + assets: [ + {id: 0, title: 'dummyText'}, + {id: 3, image: imgData}, + { + id: 5, + data: {value: 'organization.name'} + } + ], + link: {url: 'example.com'}, + imptrackers: ['tracker1.com', 'tracker2.com', 'tracker3.com'], + jstracker: 'tracker1.com' + } + }, + crid: 'crid', + ext: { + mediaType: 'native' + } + }], + }], +}; + +describe('BizzclickAdapter', function() { + describe('isBidRequestValid', function() { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(NATIVE_BID_REQUEST)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, NATIVE_BID_REQUEST); + delete bid.params; + bid.params = { + 'IncorrectParam': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('build Native Request', function () { + const request = spec.buildRequests([NATIVE_BID_REQUEST], bidRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(request).to.exist; + expect(request.method).to.exist; + expect(request.url).to.exist; + expect(request.data).to.exist; + }); + + it('sends bid request to our endpoint via POST', function () { + expect(request.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + }); + + it('Returns empty data if no valid requests are passed', function () { + let serverRequest = spec.buildRequests([]); + expect(serverRequest).to.be.an('array').that.is.empty; + }); + }); + + describe('build Banner Request', function () { + const request = spec.buildRequests([BANNER_BID_REQUEST]); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(request).to.exist; + expect(request.method).to.exist; + expect(request.url).to.exist; + expect(request.data).to.exist; + }); + + it('sends bid request to our endpoint via POST', function () { + expect(request.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + }); + }); + + describe('build Video Request', function () { + const request = spec.buildRequests([VIDEO_BID_REQUEST]); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(request).to.exist; + expect(request.method).to.exist; + expect(request.url).to.exist; + expect(request.data).to.exist; + }); + + it('sends bid request to our endpoint via POST', function () { + expect(request.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + }); + }); + + describe('interpretResponse', function () { + it('Empty response must return empty array', function() { + const emptyResponse = null; + let response = spec.interpretResponse(emptyResponse); + + expect(response).to.be.an('array').that.is.empty; + }) + + it('Should interpret banner response', function () { + const bannerResponse = { + body: [BANNER_BID_RESPONSE] + } + + const expectedBidResponse = { + requestId: BANNER_BID_RESPONSE.id, + cpm: BANNER_BID_RESPONSE.seatbid[0].bid[0].price, + width: BANNER_BID_RESPONSE.seatbid[0].bid[0].w, + height: BANNER_BID_RESPONSE.seatbid[0].bid[0].h, + ttl: BANNER_BID_RESPONSE.ttl || 1200, + currency: BANNER_BID_RESPONSE.cur || 'USD', + netRevenue: true, + creativeId: BANNER_BID_RESPONSE.seatbid[0].bid[0].crid, + dealId: BANNER_BID_RESPONSE.seatbid[0].bid[0].dealid, + mediaType: 'banner', + ad: BANNER_BID_RESPONSE.seatbid[0].bid[0].adm + } + + let bannerResponses = spec.interpretResponse(bannerResponse); + + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); + expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); + expect(dataItem.ad).to.equal(expectedBidResponse.ad); + expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); + expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(expectedBidResponse.currency); + expect(dataItem.width).to.equal(expectedBidResponse.width); + expect(dataItem.height).to.equal(expectedBidResponse.height); + }); + + it('Should interpret video response', function () { + const videoResponse = { + body: [VIDEO_BID_RESPONSE] + } + + const expectedBidResponse = { + requestId: VIDEO_BID_RESPONSE.id, + cpm: VIDEO_BID_RESPONSE.seatbid[0].bid[0].price, + width: VIDEO_BID_RESPONSE.seatbid[0].bid[0].w, + height: VIDEO_BID_RESPONSE.seatbid[0].bid[0].h, + ttl: VIDEO_BID_RESPONSE.ttl || 1200, + currency: VIDEO_BID_RESPONSE.cur || 'USD', + netRevenue: true, + creativeId: VIDEO_BID_RESPONSE.seatbid[0].bid[0].crid, + dealId: VIDEO_BID_RESPONSE.seatbid[0].bid[0].dealid, + mediaType: 'video', + vastXml: VIDEO_BID_RESPONSE.seatbid[0].bid[0].adm, + vastUrl: VIDEO_BID_RESPONSE.seatbid[0].bid[0].ext.vastUrl + } + + let videoResponses = spec.interpretResponse(videoResponse); + + expect(videoResponses).to.be.an('array').that.is.not.empty; + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastXml', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); + expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); + expect(dataItem.vastXml).to.equal(expectedBidResponse.vastXml) + expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); + expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(expectedBidResponse.currency); + expect(dataItem.width).to.equal(expectedBidResponse.width); + expect(dataItem.height).to.equal(expectedBidResponse.height); + }); + + it('Should interpret native response', function () { + const nativeResponse = { + body: [NATIVE_BID_RESPONSE] + } + + const expectedBidResponse = { + requestId: NATIVE_BID_RESPONSE.id, + cpm: NATIVE_BID_RESPONSE.seatbid[0].bid[0].price, + width: NATIVE_BID_RESPONSE.seatbid[0].bid[0].w, + height: NATIVE_BID_RESPONSE.seatbid[0].bid[0].h, + ttl: NATIVE_BID_RESPONSE.ttl || 1200, + currency: NATIVE_BID_RESPONSE.cur || 'USD', + netRevenue: true, + creativeId: NATIVE_BID_RESPONSE.seatbid[0].bid[0].crid, + dealId: NATIVE_BID_RESPONSE.seatbid[0].bid[0].dealid, + mediaType: 'native', + native: {clickUrl: NATIVE_BID_RESPONSE.seatbid[0].bid[0].adm.native.link.url} + } + + let nativeResponses = spec.interpretResponse(nativeResponse); + + expect(nativeResponses).to.be.an('array').that.is.not.empty; + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'native', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); + expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); + expect(dataItem.native.clickUrl).to.equal(expectedBidResponse.native.clickUrl) + expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); + expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(expectedBidResponse.currency); + expect(dataItem.width).to.equal(expectedBidResponse.width); + expect(dataItem.height).to.equal(expectedBidResponse.height); + }); + }); +}) diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js new file mode 100644 index 00000000000..ee37d16905b --- /dev/null +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -0,0 +1,83 @@ +import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; +import {makeSlot} from '../integration/faker/googletag.js'; + +describe('browsi Real time data sub module', function () { + const conf = { + 'auctionDelay': 250, + dataProviders: [{ + 'name': 'browsi', + 'params': { + 'url': 'testUrl.com', + 'siteKey': 'testKey', + 'pubKey': 'testPub', + 'keyName': 'bv' + } + }] + }; + + it('should init and return true', function () { + browsiRTD.collectData(); + expect(browsiRTD.browsiSubmodule.init(conf.dataProviders[0])).to.equal(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.dataProviders[0].params.keyName); + }); + + it('should match placement with ad unit', function () { + const slot = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); + + 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 + + expect(test1).to.equal(true); + expect(test2).to.equal(true); + expect(test3).to.equal(false); + expect(test4).to.equal(true); + }); + + it('should return correct macro values', function () { + const slot = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); + + 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 macroResultB = browsiRTD.getMacroId({}, slot); + expect(macroResultB).to.equal('browsiAd_1'); + + const macroResultC = browsiRTD.getMacroId({p: '', s: {s: 0, e: 1}}, slot); + expect(macroResultC).to.equal('/'); + }); + + describe('should return data to RTD module', function () { + it('should return empty if no ad units defined', function () { + browsiRTD.setData({}); + expect(browsiRTD.browsiSubmodule.getTargetingData([])).to.eql({}); + }); + + it('should return NA if no prediction for ad unit', function () { + makeSlot({code: 'adMock', divId: 'browsiAd_2'}); + browsiRTD.setData({}); + expect(browsiRTD.browsiSubmodule.getTargetingData(['adMock'])).to.eql({adMock: {bv: 'NA'}}); + }); + + it('should return prediction from server', function () { + makeSlot({code: 'hasPrediction', divId: 'hasPrediction'}); + const data = { + p: {'hasPrediction': {p: 0.234}}, + kn: 'bv', + pmd: undefined + }; + browsiRTD.setData(data); + expect(browsiRTD.browsiSubmodule.getTargetingData(['hasPrediction'])).to.eql({hasPrediction: {bv: '0.20'}}); + }) + }) +}); diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index ee4140afa10..7d3cd48a8e4 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -64,6 +64,13 @@ describe('consentManagement', function () { sinon.assert.calledOnce(utils.logWarn); sinon.assert.notCalled(utils.logInfo); }); + + it('should exit consentManagementUsp module if config is "undefined"', function() { + setConsentConfig(undefined); + expect(consentAPI).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + sinon.assert.notCalled(utils.logInfo); + }); }); describe('valid setConsentConfig value', function () { diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index deaacbc5a28..5e9b0f07f46 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -39,6 +39,12 @@ describe('consentManagement', function () { expect(userCMP).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }); + + it('should exit consentManagement module if config is "undefined"', function() { + setConsentConfig(undefined); + expect(userCMP).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }); }); describe('valid setConsentConfig value', function () { @@ -531,6 +537,15 @@ describe('consentManagement', function () { // from CMP window postMessage listener. testIFramedPage('with/JSON response', false, 'encoded_consent_data_via_post_message', 1); testIFramedPage('with/String response', true, 'encoded_consent_data_via_post_message', 1); + + it('should contain correct V1 CMP definition', (done) => { + setConsentConfig(goodConfigWithAllowAuction); + requestBidsHook(() => { + const nbArguments = window.__cmp.toString().split('\n')[0].split(', ').length; + expect(nbArguments).to.equal(3); + done(); + }, {}); + }); }); describe('v2 CMP workflow for iframe pages:', function () { @@ -556,6 +571,15 @@ describe('consentManagement', function () { testIFramedPage('with/JSON response', false, 'abc12345234', 2); testIFramedPage('with/String response', true, 'abc12345234', 2); + + it('should contain correct v2 CMP definition', (done) => { + setConsentConfig(goodConfigWithAllowAuction); + requestBidsHook(() => { + const nbArguments = window.__tcfapi.toString().split('\n')[0].split(', ').length; + expect(nbArguments).to.equal(4); + done(); + }, {}); + }); }); }); diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index aa5807da0da..65e5aaf741d 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -71,7 +71,6 @@ describe('CriteoId module', function () { }); it('should call user sync url with the right params', function () { - getCookieStub.withArgs('cto_test_cookie').returns('1'); getCookieStub.withArgs('cto_bundle').returns('bundle'); window.criteo_pubtag = {} @@ -80,7 +79,7 @@ describe('CriteoId module', function () { ajaxBuilderStub.callsFake(mockResponse(undefined, ajaxStub)) criteoIdSubmodule.getId(); - const expectedUrl = `https://gum.criteo.com/sid/json?origin=prebid&topUrl=https%3A%2F%2Ftestdev.com%2F&domain=testdev.com&bundle=bundle&cw=1&pbt=1`; + const expectedUrl = `https://gum.criteo.com/sid/json?origin=prebid&topUrl=https%3A%2F%2Ftestdev.com%2F&domain=testdev.com&bundle=bundle&cw=1&pbt=1&lsw=1`; expect(ajaxStub.calledWith(expectedUrl)).to.be.true; diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index c0ecb9cad5e..ed9c968cfa2 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -7,6 +7,7 @@ import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { targeting } from 'src/targeting.js'; import { auctionManager } from 'src/auctionManager.js'; +import { uspDataHandler } from 'src/adapterManager.js'; import * as adpod from 'modules/adpod.js'; import { server } from 'test/mocks/xhr.js'; @@ -115,6 +116,44 @@ describe('The DFP video support module', function () { expect(customParams).to.have.property('hb_cache_id', bid.videoCacheKey); }); + it('should include the us_privacy key when USP Consent is available', function () { + let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspDataHandlerStub.returns('1YYY'); + + const bidCopy = utils.deepClone(bid); + bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + hb_adid: 'ad_id', + }); + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + 'iu': 'my/adUnit' + } + })); + const queryObject = utils.parseQS(url.query); + expect(queryObject.us_privacy).to.equal('1YYY'); + uspDataHandlerStub.restore(); + }); + + it('should not include the us_privacy key when USP Consent is not available', function () { + const bidCopy = utils.deepClone(bid); + bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + hb_adid: 'ad_id', + }); + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + 'iu': 'my/adUnit' + } + })); + const queryObject = utils.parseQS(url.query); + expect(queryObject.us_privacy).to.equal(undefined); + }); + describe('special targeting unit test', function () { const allTargetingData = { 'hb_format': 'video', @@ -350,6 +389,8 @@ describe('The DFP video support module', function () { it('should return masterTag url', function() { amStub.returns(getBidsReceived()); + let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspDataHandlerStub.returns('1YYY'); let url; parse(buildAdpodVideoUrl({ code: 'adUnitCode-1', @@ -380,10 +421,12 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('unviewed_position_start', '1'); expect(queryParams).to.have.property('url'); expect(queryParams).to.have.property('cust_params'); + expect(queryParams).to.have.property('us_privacy', '1YYY'); const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); expect(custParams).to.have.property('hb_cache_id', '123'); expect(custParams).to.have.property('hb_pb_cat_dur', '15.00_395_15s,15.00_406_30s,10.00_395_15s'); + uspDataHandlerStub.restore(); } }); diff --git a/test/spec/modules/districtmDmxBidAdapter_spec.js b/test/spec/modules/districtmDmxBidAdapter_spec.js index 90e6957fc2c..5d1f299dad5 100644 --- a/test/spec/modules/districtmDmxBidAdapter_spec.js +++ b/test/spec/modules/districtmDmxBidAdapter_spec.js @@ -1,6 +1,6 @@ -import {expect} from 'chai'; +import { expect } from 'chai'; import * as _ from 'lodash'; -import {spec, matchRequest, checkDeepArray, defaultSize, upto5, cleanSizes, shuffle, getApi, bindUserId, getPlaybackmethod, getProtocols, cleanVast} from '../../../modules/districtmDMXBidAdapter.js'; +import { spec, matchRequest, checkDeepArray, defaultSize, upto5, cleanSizes, shuffle, getApi, bindUserId, getPlaybackmethod, getProtocols, cleanVast } from '../../../modules/districtmDMXBidAdapter.js'; const sample_vast = ` @@ -144,8 +144,12 @@ const bidRequestVideo = [{ 'video/mp4'], } }, - 'mediaTypes': { video: {context: 'instream', // or 'outstream' - playerSize: [[640, 480]]} }, + 'mediaTypes': { + video: { + context: 'instream', // or 'outstream' + playerSize: [[640, 480]] + } + }, 'adUnitCode': 'div-gpt-ad-12345678-1', 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', 'sizes': [ @@ -602,53 +606,53 @@ const emptyResponseSeatBid = { body: { seatbid: [] } }; describe('DistrictM Adaptor', function () { const districtm = spec; - describe('verification of upto5', function() { - it('upto5 function should always break 12 imps into 3 request same for 15', function() { + describe('verification of upto5', function () { + it('upto5 function should always break 12 imps into 3 request same for 15', function () { expect(upto5([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], bidRequest, bidderRequest, 'https://google').length).to.be.equal(3) 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() { + 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() { + describe('Test getApi function', function () { const data = { - protocols: ['VPAID_1_0'] + api: [1] } - it('Will return 1 for vpaid version 1', function() { + 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() { + 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() { + 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() { + 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() { + describe('Test getPlaybackmethod function', function () { + it('getPlaybackmethod will return 2', function () { expect(getPlaybackmethod([])[0]).to.be.equal(2) }) - it('getPlaybackmethod will return 6', function() { + 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) + describe('Test getProtocols function', function () { + it('getProtocols will return 3', function () { + expect(getProtocols({ protocols: [3] })[0]).to.be.equal(3) }) - it('getProtocols will return 6', function() { + it('getProtocols will return 6', function () { expect(_.isEqual(getProtocols({}), [2, 3, 5, 6, 7, 8])).to.be.equal(true) }) }) @@ -687,7 +691,7 @@ describe('DistrictM Adaptor', function () { memberid: 10003, }; it(`function should return true`, function () { - expect(districtm.isBidRequestValid({params})).to.be.equal(true); + expect(districtm.isBidRequestValid({ params })).to.be.equal(true); }); it(`function should return false`, function () { expect(districtm.isBidRequestValid({ params: { memberid: 12345 } })).to.be.equal(false); @@ -716,19 +720,19 @@ describe('DistrictM Adaptor', function () { it(`the function should return an array`, function () { expect(buildRequestResults).to.be.an('object'); }); - it(`contain gdpr consent & ccpa`, function() { + it(`contain gdpr consent & ccpa`, function () { const bidr = JSON.parse(buildRequestResults.data) expect(bidr.regs.ext.gdpr).to.be.equal(1); expect(bidr.regs.ext.us_privacy).to.be.equal('1NY'); expect(bidr.user.ext.consent).to.be.an('string'); }); - it(`test contain COPPA`, function() { + it(`test contain COPPA`, function () { const bidr = JSON.parse(buildRequestResults.data) bidr.regs = bidr.regs || {}; bidr.regs.coppa = 1; expect(bidr.regs.coppa).to.be.equal(1) }) - it(`test should not contain COPPA`, function() { + it(`test should not contain COPPA`, function () { const bidr = JSON.parse(buildRequestResultsNoCoppa.data) expect(bidr.regs.coppa).to.be.equal(0) }) @@ -737,17 +741,17 @@ describe('DistrictM Adaptor', function () { }); }); - describe('bidRequest Video testing', 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}); - const emptyResponseResultsNegation = districtm.interpretResponse(responsesNegative, {bidderRequest}); - const emptyResponseResultsEmptySeat = districtm.interpretResponse(emptyResponseSeatBid, {bidderRequest}); + const responseResults = districtm.interpretResponse(responses, { bidderRequest }); + const emptyResponseResults = districtm.interpretResponse(emptyResponse, { bidderRequest }); + const emptyResponseResultsNegation = districtm.interpretResponse(responsesNegative, { bidderRequest }); + const emptyResponseResultsEmptySeat = districtm.interpretResponse(emptyResponseSeatBid, { bidderRequest }); it(`the function should return an array`, function () { expect(responseResults).to.be.an('array'); }); @@ -767,10 +771,10 @@ describe('DistrictM Adaptor', function () { }); describe(`check validation for id sync gdpr ccpa`, () => { - let allin = spec.getUserSyncs({iframeEnabled: true}, {}, bidderRequest.gdprConsent, bidderRequest.uspConsent)[0] - let noCCPA = spec.getUserSyncs({iframeEnabled: true}, {}, bidderRequest.gdprConsent, null)[0] - let noGDPR = spec.getUserSyncs({iframeEnabled: true}, {}, null, bidderRequest.uspConsent)[0] - let nothing = spec.getUserSyncs({iframeEnabled: true}, {}, null, null)[0] + let allin = spec.getUserSyncs({ iframeEnabled: true }, {}, bidderRequest.gdprConsent, bidderRequest.uspConsent)[0] + let noCCPA = spec.getUserSyncs({ iframeEnabled: true }, {}, bidderRequest.gdprConsent, null)[0] + let noGDPR = spec.getUserSyncs({ iframeEnabled: true }, {}, null, bidderRequest.uspConsent)[0] + let nothing = spec.getUserSyncs({ iframeEnabled: true }, {}, null, null)[0] /* @@ -793,10 +797,10 @@ describe('DistrictM Adaptor', function () { }) describe(`Helper function testing`, function () { - const bid = matchRequest('29a28a1bbc8a8d', {bidderRequest}); - const {width, height} = defaultSize(bid); + const bid = matchRequest('29a28a1bbc8a8d', { bidderRequest }); + const { width, height } = defaultSize(bid); it(`test matchRequest`, function () { - expect(matchRequest('29a28a1bbc8a8d', {bidderRequest})).to.be.an('object'); + expect(matchRequest('29a28a1bbc8a8d', { bidderRequest })).to.be.an('object'); }); it(`test checkDeepArray`, function () { expect(_.isEqual(checkDeepArray([728, 90]), [728, 90])).to.be.equal(true); diff --git a/test/spec/modules/emx_digitalBidAdapter_spec.js b/test/spec/modules/emx_digitalBidAdapter_spec.js index 138786b9c74..39e56638ece 100644 --- a/test/spec/modules/emx_digitalBidAdapter_spec.js +++ b/test/spec/modules/emx_digitalBidAdapter_spec.js @@ -367,6 +367,27 @@ describe('emx_digital Adapter', function () { expect(request.us_privacy).to.exist; expect(request.us_privacy).to.exist.and.to.equal(consentString); }); + + it('should add schain object to request', function() { + const schainBidderRequest = utils.deepClone(bidderRequest); + schainBidderRequest.schain = { + 'complete': 1, + 'ver': '1.0', + 'nodes': [ + { + 'asi': 'testing.com', + 'sid': 'abc', + 'hp': 1 + } + ] + }; + let request = spec.buildRequests(schainBidderRequest.bids, schainBidderRequest); + request = JSON.parse(request.data); + expect(request.source.ext.schain).to.exist; + expect(request.source.ext.schain).to.have.property('complete', 1); + expect(request.source.ext.schain).to.have.property('ver', '1.0'); + expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.schain.nodes[0].asi); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/etargetBidAdapter_spec.js b/test/spec/modules/etargetBidAdapter_spec.js index 4f5e0c224ec..2dbf6cd68c5 100644 --- a/test/spec/modules/etargetBidAdapter_spec.js +++ b/test/spec/modules/etargetBidAdapter_spec.js @@ -154,6 +154,7 @@ describe('etarget adapter', function () { assert.equal(result.height, 250); assert.equal(result.currency, 'EUR'); assert.equal(result.netRevenue, true); + assert.isNotNull(result.reason); assert.equal(result.ttl, 360); assert.equal(result.ad, ''); assert.equal(result.transactionId, '5f33781f-9552-4ca1'); diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index 3047b635d13..c44d7908ba8 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -152,6 +152,19 @@ describe('freewheelSSP BidAdapter Test', () => { expect(payload.playerSize).to.equal('300x600'); expect(payload._fw_gdpr_consent).to.exist.and.to.be.a('string'); expect(payload._fw_gdpr_consent).to.equal(gdprConsentString); + + let gdprConsent = { + 'gdprApplies': true, + 'consentString': gdprConsentString + } + let syncOptions = { + 'pixelEnabled': true + } + const userSyncs = spec.getUserSyncs(syncOptions, null, gdprConsent, null); + expect(userSyncs).to.deep.equal([{ + type: 'image', + url: 'https://ads.stickyadstv.com/auto-user-sync?gdpr=1&gdpr_consent=1FW-SSP-gdprConsent-' + }]); }); }) @@ -226,6 +239,19 @@ describe('freewheelSSP BidAdapter Test', () => { expect(payload.playerSize).to.equal('300x600'); expect(payload._fw_gdpr_consent).to.exist.and.to.be.a('string'); expect(payload._fw_gdpr_consent).to.equal(gdprConsentString); + + let gdprConsent = { + 'gdprApplies': true, + 'consentString': gdprConsentString + } + let syncOptions = { + 'pixelEnabled': true + } + const userSyncs = spec.getUserSyncs(syncOptions, null, gdprConsent, null); + expect(userSyncs).to.deep.equal([{ + type: 'image', + url: 'https://ads.stickyadstv.com/auto-user-sync?gdpr=1&gdpr_consent=1FW-SSP-gdprConsent-' + }]); }); }) diff --git a/test/spec/modules/geoedgeRtdProvider_spec.js b/test/spec/modules/geoedgeRtdProvider_spec.js new file mode 100644 index 00000000000..cf4e0b53fde --- /dev/null +++ b/test/spec/modules/geoedgeRtdProvider_spec.js @@ -0,0 +1,111 @@ +import * as utils from '../../../src/utils.js'; +import * as hook from '../../../src/hook.js' +import { beforeInit, geoedgeSubmodule, setWrapper, wrapper, htmlPlaceholder, WRAPPER_URL, getClientUrl } from '../../../modules/geoedgeRtdProvider.js'; +import { server } from '../../../test/mocks/xhr.js'; + +let key = '123123123'; +function makeConfig() { + return { + name: 'geoedge', + params: { + wap: false, + key: key, + bidders: { + bidderA: true, + bidderB: false + } + } + }; +} + +function mockBid(bidderCode) { + return { + 'ad': '', + 'cpm': '1.00', + 'width': 300, + 'height': 250, + 'bidderCode': bidderCode, + 'requestId': utils.getUniqueIdentifierStr(), + 'creativeId': 'id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }; +} + +let mockWrapper = `${htmlPlaceholder}`; + +describe('Geoedge RTD module', function () { + describe('beforeInit', function () { + let submoduleStub; + + before(function () { + submoduleStub = sinon.stub(hook, 'submodule'); + }); + after(function () { + submoduleStub.restore(); + }); + it('should fetch the wrapper', function () { + beforeInit(); + let request = server.requests[0]; + let isWrapperRequest = request && request.url && request.url && request.url === WRAPPER_URL; + expect(isWrapperRequest).to.equal(true); + }); + it('should register RTD submodule provider', function () { + expect(submoduleStub.calledWith('realTimeData', geoedgeSubmodule)).to.equal(true); + }); + }); + describe('setWrapper', function () { + it('should set the wrapper', function () { + setWrapper(mockWrapper); + expect(wrapper).to.equal(mockWrapper); + }); + }); + describe('submodule', function () { + describe('name', function () { + it('should be geoedge', function () { + expect(geoedgeSubmodule.name).to.equal('geoedge'); + }); + }); + describe('init', function () { + let insertElementStub; + + before(function () { + insertElementStub = sinon.stub(utils, 'insertElement'); + }); + after(function () { + utils.insertElement.restore(); + }); + it('should return false when missing params or key', function () { + let missingParams = geoedgeSubmodule.init({}); + let missingKey = geoedgeSubmodule.init({ params: {} }); + expect(missingParams || missingKey).to.equal(false); + }); + it('should return true when params are ok', function () { + expect(geoedgeSubmodule.init(makeConfig())).to.equal(true); + }); + it('should preload the client', function () { + let isLinkPreloadAsScript = arg => arg.tagName === 'LINK' && arg.rel === 'preload' && arg.as === 'script' && arg.href === getClientUrl(key); + expect(insertElementStub.calledWith(sinon.match(isLinkPreloadAsScript))).to.equal(true); + }); + }); + describe('onBidResponseEvent', function () { + let bidFromA = mockBid('bidderA'); + it('should wrap bid html when bidder is configured', function () { + geoedgeSubmodule.onBidResponseEvent(bidFromA, makeConfig()); + expect(bidFromA.ad.indexOf('')).to.equal(0); + }); + it('should not wrap bid html when bidder is not configured', function () { + let bidFromB = mockBid('bidderB'); + geoedgeSubmodule.onBidResponseEvent(bidFromB, makeConfig()); + expect(bidFromB.ad.indexOf('')).to.equal(-1); + }); + it('should only muatate the bid ad porperty', function () { + let copy = Object.assign({}, bidFromA); + delete copy.ad; + let equalsOriginal = Object.keys(copy).every(key => copy[key] === bidFromA[key]); + expect(equalsOriginal).to.equal(true); + }); + }); + }); +}); diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index 1cfca4779ad..e884df40c5e 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -40,229 +40,6 @@ describe('TheMediaGrid Adapter', function () { }); describe('buildRequests', function () { - function parseRequest(url) { - const res = {}; - url.split('&').forEach((it) => { - const couple = it.split('='); - res[couple[0]] = decodeURIComponent(couple[1]); - }); - return res; - } - const bidderRequest = {refererInfo: {referer: 'https://example.com'}}; - const referrer = bidderRequest.refererInfo.referer; - let bidRequests = [ - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-2', - 'sizes': [[728, 90]], - 'mediaTypes': { - 'video': { - 'playerSize': [400, 600] - }, - 'banner': { - 'sizes': [[728, 90]] - } - }, - 'bidId': '3150ccb55da321', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '2' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'mediaTypes': { - 'video': { - 'playerSize': [400, 600] - }, - 'banner': { - 'sizes': [[300, 250], [300, 600]] - } - }, - 'bidId': '42dbe3a7168a6a', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - } - ]; - - it('should attach valid params to the tag', function () { - const [request] = spec.buildRequests([bidRequests[0]], bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('auids', '1'); - expect(payload).to.have.property('sizes', '300x250,300x600'); - expect(payload).to.have.property('r', '22edbae2733bf6'); - expect(payload).to.have.property('wrapperType', 'Prebid_js'); - expect(payload).to.have.property('wrapperVersion', '$prebid.version$'); - }); - - it('sizes must be added from mediaTypes', function () { - const [request] = spec.buildRequests([bidRequests[0], bidRequests[1]], bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('auids', '1,1'); - expect(payload).to.have.property('sizes', '300x250,300x600,728x90,400x600'); - expect(payload).to.have.property('r', '22edbae2733bf6'); - }); - - it('sizes must not be duplicated', function () { - const [request] = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('auids', '1,1,2'); - expect(payload).to.have.property('sizes', '300x250,300x600,728x90,400x600'); - expect(payload).to.have.property('r', '22edbae2733bf6'); - }); - - it('if gdprConsent is present payload must have gdpr params', function () { - const [request] = spec.buildRequests(bidRequests, {gdprConsent: {consentString: 'AAA', gdprApplies: true}, refererInfo: bidderRequest.refererInfo}); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('gdpr_consent', 'AAA'); - expect(payload).to.have.property('gdpr_applies', '1'); - }); - - it('if gdprApplies is false gdpr_applies must be 0', function () { - const [request] = spec.buildRequests(bidRequests, {gdprConsent: {consentString: 'AAA', gdprApplies: false}}); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('gdpr_consent', 'AAA'); - expect(payload).to.have.property('gdpr_applies', '0'); - }); - - it('if gdprApplies is undefined gdpr_applies must be 1', function () { - const [request] = spec.buildRequests(bidRequests, {gdprConsent: {consentString: 'AAA'}}); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('gdpr_consent', 'AAA'); - expect(payload).to.have.property('gdpr_applies', '1'); - }); - - it('if usPrivacy is present payload must have us_privacy param', function () { - const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); - const [request] = spec.buildRequests(bidRequests, bidderRequestWithUSP); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('us_privacy', '1YNN'); - }); - - it('should convert keyword params to proper form and attaches to request', function () { - const bidRequestWithKeywords = [].concat(bidRequests); - bidRequestWithKeywords[1] = Object.assign({}, - bidRequests[1], - { - params: { - uid: '1', - keywords: { - single: 'val', - singleArr: ['val'], - singleArrNum: [3], - multiValMixed: ['value1', 2, 'value3'], - singleValNum: 123, - emptyStr: '', - emptyArr: [''], - badValue: {'foo': 'bar'} // should be dropped - } - } - } - ); - - const [request] = spec.buildRequests(bidRequestWithKeywords, bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload.keywords).to.be.an('string'); - payload.keywords = JSON.parse(payload.keywords); - - expect(payload.keywords).to.deep.equal([{ - 'key': 'single', - 'value': ['val'] - }, { - 'key': 'singleArr', - 'value': ['val'] - }, { - 'key': 'singleArrNum', - 'value': ['3'] - }, { - 'key': 'multiValMixed', - 'value': ['value1', '2', 'value3'] - }, { - 'key': 'singleValNum', - 'value': ['123'] - }, { - 'key': 'emptyStr' - }, { - 'key': 'emptyArr' - }]); - }); - - it('should mix keyword param with keywords from config', function () { - const getConfigStub = sinon.stub(config, 'getConfig').callsFake( - arg => arg === 'fpd.user' ? {'keywords': ['a', 'b']} : arg === 'fpd.context' ? {'keywords': ['any words']} : null); - - const bidRequestWithKeywords = [].concat(bidRequests); - bidRequestWithKeywords[1] = Object.assign({}, - bidRequests[1], - { - params: { - uid: '1', - keywords: { - single: 'val', - singleArr: ['val'], - multiValMixed: ['value1', 2, 'value3'] - } - } - } - ); - - const [request] = spec.buildRequests(bidRequestWithKeywords, bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload.keywords).to.be.an('string'); - payload.keywords = JSON.parse(payload.keywords); - - expect(payload.keywords).to.deep.equal([{ - 'key': 'single', - 'value': ['val'] - }, { - 'key': 'singleArr', - 'value': ['val'] - }, { - 'key': 'multiValMixed', - 'value': ['value1', '2', 'value3'] - }, { - 'key': 'user', - 'value': ['a', 'b'] - }, { - 'key': 'context', - 'value': ['any words'] - }]); - - getConfigStub.restore(); - }); - }); - - describe('buildRequests in new format', function () { function parseRequest(data) { return JSON.parse(data); } @@ -278,7 +55,7 @@ describe('TheMediaGrid Adapter', function () { 'bidder': 'grid', 'params': { 'uid': '1', - 'useNewFormat': true + 'bidFloor': 1.25 }, 'adUnitCode': 'adunit-code-1', 'sizes': [[300, 250], [300, 600]], @@ -294,8 +71,7 @@ describe('TheMediaGrid Adapter', function () { { 'bidder': 'grid', 'params': { - 'uid': '2', - 'useNewFormat': true + 'uid': '2' }, 'adUnitCode': 'adunit-code-1', 'sizes': [[300, 250], [300, 600]], @@ -306,8 +82,7 @@ describe('TheMediaGrid Adapter', function () { { 'bidder': 'grid', 'params': { - 'uid': '11', - 'useNewFormat': true + 'uid': '11' }, 'adUnitCode': 'adunit-code-2', 'sizes': [[728, 90]], @@ -324,8 +99,7 @@ describe('TheMediaGrid Adapter', function () { { 'bidder': 'grid', 'params': { - 'uid': '3', - 'useNewFormat': true + 'uid': '3' }, 'adUnitCode': 'adunit-code-2', 'sizes': [[728, 90]], @@ -344,7 +118,7 @@ describe('TheMediaGrid Adapter', function () { ]; it('should attach valid params to the tag', function () { - const [request] = spec.buildRequests([bidRequests[0]], bidderRequest); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -361,6 +135,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -371,7 +146,7 @@ describe('TheMediaGrid Adapter', function () { }); it('make possible to process request without mediaTypes', function () { - const [request] = spec.buildRequests([bidRequests[0], bidRequests[1]], bidderRequest); + const request = spec.buildRequests([bidRequests[0], bidRequests[1]], bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -388,6 +163,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -407,7 +183,7 @@ describe('TheMediaGrid Adapter', function () { }); it('should attach valid params to the video tag', function () { - const [request] = spec.buildRequests(bidRequests.slice(0, 3), bidderRequest); + const request = spec.buildRequests(bidRequests.slice(0, 3), bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -424,6 +200,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -452,7 +229,7 @@ describe('TheMediaGrid Adapter', function () { }); it('should support mixed mediaTypes', function () { - const [request] = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -469,6 +246,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -511,7 +289,7 @@ describe('TheMediaGrid Adapter', function () { it('if gdprConsent is present payload must have gdpr params', function () { const gdprBidderRequest = Object.assign({gdprConsent: {consentString: 'AAA', gdprApplies: true}}, bidderRequest); - const [request] = spec.buildRequests(bidRequests, gdprBidderRequest); + const request = spec.buildRequests(bidRequests, gdprBidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('user'); @@ -524,7 +302,7 @@ describe('TheMediaGrid Adapter', function () { it('if usPrivacy is present payload must have us_privacy param', function () { const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); - const [request] = spec.buildRequests(bidRequests, bidderRequestWithUSP); + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('regs'); @@ -536,22 +314,65 @@ describe('TheMediaGrid Adapter', function () { const bidRequestsWithUserIds = bidRequests.map((bid) => { return Object.assign({ userId: { - id5id: { uid: 'id5id_1' }, + id5id: { uid: 'id5id_1', ext: { linkType: 2 } }, tdid: 'tdid_1', digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - lipb: {lipbid: 'lipb_1'} + lipb: {lipbid: 'lipb_1'}, + idl_env: 'idl_env_1', + criteoId: 'criteoId_1' } }, bid); }); - const [request] = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); + const request = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('user'); expect(payload.user).to.have.property('ext'); - expect(payload.user.ext).to.have.property('unifiedid', 'tdid_1'); - expect(payload.user.ext).to.have.property('id5id', 'id5id_1'); - expect(payload.user.ext).to.have.property('digitrustid', 'DTID'); - expect(payload.user.ext).to.have.property('liveintentid', 'lipb_1'); + expect(payload.user.ext.digitrust).to.deep.equal({ + id: 'DTID', + keyv: 4, + privacy: { + optout: false + }, + producer: 'ABC', + version: 2 + }); + expect(payload.user.ext.eids).to.deep.equal([ + { + source: 'adserver.org', + uids: [{ + id: 'tdid_1', + ext: { + rtiPartner: 'TDID' + } + }] + }, + { + source: 'id5-sync.com', + uids: [{ + id: 'id5id_1' + }], + ext: { linkType: 2 } + }, + { + source: 'liveintent.com', + uids: [{ + id: 'lipb_1' + }] + }, + { + source: 'identityLink', + uids: [{ + id: 'idl_env_1' + }] + }, + { + source: 'criteo.com', + uids: [{ + id: 'criteoId_1' + }] + } + ]); }); it('if schain is present payload must have source.ext.schain param', function () { @@ -570,7 +391,7 @@ describe('TheMediaGrid Adapter', function () { schain: schain }, bid); }); - const [request] = spec.buildRequests(bidRequestsWithSChain, bidderRequest); + const request = spec.buildRequests(bidRequestsWithSChain, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('source'); @@ -579,20 +400,22 @@ describe('TheMediaGrid Adapter', function () { expect(payload.source.ext.schain).to.deep.equal(schain); }); - it('if content and segment is present in realTimeData.jwTargeting, payload must have right params', function () { + it('if content and segment is present in jwTargeting, payload must have right params', function () { const jsContent = {id: 'test_jw_content_id'}; const jsSegments = ['test_seg_1', 'test_seg_2']; const bidRequestsWithUserIds = bidRequests.map((bid) => { return Object.assign({ - realTimeData: { - jwTargeting: { - segments: jsSegments, - content: jsContent + rtd: { + jwplayer: { + targeting: { + segments: jsSegments, + content: jsContent + } } } }, bid); }); - const [request] = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); + const request = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('user'); @@ -606,14 +429,68 @@ describe('TheMediaGrid Adapter', function () { expect(payload).to.have.property('site'); expect(payload.site.content).to.deep.equal(jsContent); }); + + it('shold be right tmax when timeout in config is less then timeout in bidderRequest', function() { + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'bidderTimeout' ? 2000 : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.tmax).to.equal(2000); + getConfigStub.restore(); + }); + it('shold be right tmax when timeout in bidderRequest is less then timeout in config', function() { + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'bidderTimeout' ? 5000 : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.tmax).to.equal(3000); + getConfigStub.restore(); + }); + describe('floorModule', function () { + const floorTestData = { + 'currency': 'USD', + 'floor': 1.50 + }; + const bidRequest = Object.assign({ + getFloor: (_) => { + return floorTestData; + } + }, bidRequests[1]); + it('should return the value from getFloor if present', function () { + const request = spec.buildRequests([bidRequest], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].bidfloor).to.equal(floorTestData.floor); + }); + it('should return the getFloor.floor value if it is greater than bidfloor', function () { + const bidfloor = 0.80; + const bidRequestsWithFloor = { ...bidRequest }; + bidRequestsWithFloor.params = Object.assign({bidFloor: bidfloor}, bidRequestsWithFloor.params); + const request = spec.buildRequests([bidRequestsWithFloor], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].bidfloor).to.equal(floorTestData.floor); + }); + it('should return the bidfloor value if it is greater than getFloor.floor', function () { + const bidfloor = 1.80; + const bidRequestsWithFloor = { ...bidRequest }; + bidRequestsWithFloor.params = Object.assign({bidFloor: bidfloor}, bidRequestsWithFloor.params); + const request = spec.buildRequests([bidRequestsWithFloor], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].bidfloor).to.equal(bidfloor); + }); + }); }); describe('interpretResponse', function () { const responses = [ - {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, dealid: 11}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300}], 'seat': '1'}, - {'bid': [{'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, - {'bid': [{'price': 0, 'auid': 3, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'impid': '659423fff799cb', 'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, 'dealid': 11}], 'seat': '1'}, + {'bid': [{'impid': '4dff80cc4ee346', 'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'impid': '5703af74d0472a', 'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'impid': '2344da98f78b42', 'price': 0, 'auid': 3, 'h': 250, 'w': 300}], 'seat': '1'}, {'bid': [{'price': 0, 'adm': '
test content 5
', 'h': 250, 'w': 300}], 'seat': '1'}, undefined, {'bid': [], 'seat': '1'}, @@ -634,7 +511,7 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '1cbd2feafe5e8b', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { 'requestId': '659423fff799cb', @@ -665,7 +542,7 @@ describe('TheMediaGrid Adapter', function () { }, 'adUnitCode': 'adunit-code-1', 'sizes': [[300, 250], [300, 600]], - 'bidId': '300bfeb0d71a5b', + 'bidId': '659423fff799cb', 'bidderRequestId': '2c2bb1972df9a', 'auctionId': '1fa09aee5c8c99', }, @@ -692,10 +569,10 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '1fa09aee5c8c99', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { - 'requestId': '300bfeb0d71a5b', + 'requestId': '659423fff799cb', 'cpm': 1.15, 'creativeId': 1, 'dealId': 11, @@ -778,10 +655,10 @@ describe('TheMediaGrid Adapter', function () { } ]; const response = [ - {'bid': [{'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 11, content_type: 'video', w: 300, h: 600}], 'seat': '2'}, - {'bid': [{'price': 1.00, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 12, content_type: 'video'}], 'seat': '2'} + {'bid': [{'impid': '659423fff799cb', 'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 11, content_type: 'video', w: 300, h: 600}], 'seat': '2'}, + {'bid': [{'impid': '2bc598e42b6a', 'price': 1.00, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 12, content_type: 'video'}], 'seat': '2'} ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { 'requestId': '659423fff799cb', @@ -799,6 +676,23 @@ describe('TheMediaGrid Adapter', function () { 'adResponse': { 'content': '\n<\/Ad>\n<\/VAST>' } + }, + { + 'requestId': '2bc598e42b6a', + 'cpm': 1.00, + 'creativeId': 12, + 'dealId': undefined, + 'width': undefined, + 'height': undefined, + 'bidderCode': 'grid', + 'currency': 'USD', + 'mediaType': 'video', + 'netRevenue': false, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + } } ]; @@ -842,18 +736,18 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '1fa09aee5c84d34', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const result = spec.interpretResponse({'body': {'seatbid': responses.slice(2)}}, request); expect(result.length).to.equal(0); }); it('complicated case', function () { const fullResponse = [ - {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, dealid: 11}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300, dealid: 12}], 'seat': '1'}, - {'bid': [{'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, - {'bid': [{'price': 0.15, 'adm': '
test content 4
', 'auid': 1, 'h': 600, 'w': 300}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 5
', 'auid': 2, 'h': 600, 'w': 350}], 'seat': '1'}, + {'bid': [{'impid': '2164be6358b9', 'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, dealid: 11}], 'seat': '1'}, + {'bid': [{'impid': '4e111f1b66e4', 'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300, dealid: 12}], 'seat': '1'}, + {'bid': [{'impid': '26d6f897b516', 'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'impid': '326bde7fbf69', 'price': 0.15, 'adm': '
test content 4
', 'auid': 1, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'impid': '2234f233b22a', 'price': 0.5, 'adm': '
test content 5
', 'auid': 2, 'h': 600, 'w': 350}], 'seat': '1'}, ]; const bidRequests = [ { @@ -912,7 +806,7 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '32a1f276cb87cb8', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { 'requestId': '2164be6358b9', @@ -975,82 +869,6 @@ describe('TheMediaGrid Adapter', function () { const result = spec.interpretResponse({'body': {'seatbid': fullResponse}}, request); expect(result).to.deep.equal(expectedResponse); }); - - it('dublicate uids and sizes in one slot', function () { - const fullResponse = [ - {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 1, 'h': 250, 'w': 300}], 'seat': '1'}, - ]; - const bidRequests = [ - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '5126e301f4be', - 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '57b2ebe70e16', - 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '225fcd44b18c', - 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', - } - ]; - const [request] = spec.buildRequests(bidRequests); - const expectedResponse = [ - { - 'requestId': '5126e301f4be', - 'cpm': 1.15, - 'creativeId': 1, - 'dealId': undefined, - 'width': 300, - 'height': 250, - 'ad': '
test content 1
', - 'bidderCode': 'grid', - 'currency': 'USD', - 'mediaType': 'banner', - 'netRevenue': false, - 'ttl': 360, - }, - { - 'requestId': '57b2ebe70e16', - 'cpm': 0.5, - 'creativeId': 1, - 'dealId': undefined, - 'width': 300, - 'height': 250, - 'ad': '
test content 2
', - 'bidderCode': 'grid', - 'currency': 'USD', - 'mediaType': 'banner', - 'netRevenue': false, - 'ttl': 360, - } - ]; - - const result = spec.interpretResponse({'body': {'seatbid': fullResponse}}, request); - expect(result).to.deep.equal(expectedResponse); - }); }); describe('user sync', function () { diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index 701ce9a7e81..52a3a21db4e 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { spec } from 'modules/gumgumBidAdapter.js'; +import { BANNER, VIDEO } from 'src/mediaTypes.js'; const ENDPOINT = 'https://g2.gumgum.com/hbid/imp'; const JCSI = { t: 0, rq: 8, pbv: '$prebid.version$' } @@ -462,16 +463,15 @@ describe('gumgumAdapter', function () { pi: 3 } let expectedResponse = { - 'ad': '

I am an ad

', - 'cpm': 0, - 'creativeId': 29593, - 'currency': 'USD', - 'height': '250', - 'netRevenue': true, - 'requestId': 12345, - 'width': '300', - // dealId: DEAL_ID, - // referrer: REFERER, + ad: '

I am an ad

', + cpm: 0, + creativeId: 29593, + currency: 'USD', + height: '250', + netRevenue: true, + requestId: 12345, + width: '300', + mediaType: BANNER, ttl: 60 }; @@ -552,6 +552,20 @@ describe('gumgumAdapter', function () { const decodedResponse = JSON.parse(atob(bidResponse)); expect(decodedResponse.jcsi).to.eql(JCSI); }); + + it('sets the correct mediaType depending on product', function () { + const bannerBidResponse = spec.interpretResponse({ body: serverResponse }, bidRequest)[0]; + const invideoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 6 } })[0]; + const videoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 7 } })[0]; + expect(bannerBidResponse.mediaType).to.equal(BANNER); + expect(invideoBidResponse.mediaType).to.equal(VIDEO); + expect(videoBidResponse.mediaType).to.equal(VIDEO); + }); + + it('sets a vastXml property if mediaType is video', function () { + const videoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 7 } })[0]; + expect(videoBidResponse.vastXml).to.exist; + }); }) describe('getUserSyncs', function () { const syncOptions = { diff --git a/test/spec/modules/haloRtdProvider_spec.js b/test/spec/modules/haloRtdProvider_spec.js new file mode 100644 index 00000000000..69ea4bc997c --- /dev/null +++ b/test/spec/modules/haloRtdProvider_spec.js @@ -0,0 +1,220 @@ +import { HALOID_LOCAL_NAME, SEG_LOCAL_NAME, addSegmentData, getSegments, haloSubmodule, storage } from 'modules/haloRtdProvider.js'; +import { server } from 'test/mocks/xhr.js'; + +const responseHeader = {'Content-Type': 'application/json'}; + +describe('haloRtdProvider', function() { + describe('haloSubmodule', function() { + it('successfully instantiates', function () { + expect(haloSubmodule.init()).to.equal(true); + }); + }); + + describe('Add Segment Data', function() { + it('adds segment data', function() { + const config = { + params: { + mapSegments: { + 'appnexus': true, + 'generic': true + } + } + }; + + let adUnits = [ + { + bids: [ + // bid with existing segment data in bid obj and fpd + { + bidder: 'appnexus', + fpd: { + user: { + data: [ + { + id: 'appnexus', + segment: [ + { + id: '0' + } + ] + } + ] + } + }, + params: { + user: { + segments: [0] + } + } + } + ] + }, + + // bids with fpd data definitions but without existing segment data + { + bids: [ + { + bidder: 'appnexus', + fpd: { + user: { + data: [ + { + id: 'appnexus' + } + ] + } + } + }, + { + bidder: 'generic', + fpd: { + user: { + data: [ + { + id: 'generic' + } + ] + } + } + } + ] + } + ]; + + const data = { + appnexus: [{id: '1'}, {id: '2'}, {id: '3'}], + generic: [{id: 'seg1'}, {id: 'seg2'}, {id: 'seg3'}] + }; + + addSegmentData(adUnits, data, config); + + expect(adUnits[0].bids[0].fpd.user.data[0].segment[0]).to.have.deep.property('id', '0'); + expect(adUnits[0].bids[0].fpd.user.data[0].segment[1]).to.have.deep.property('id', '1'); + expect(adUnits[0].bids[0].fpd.user.data[0].segment[2]).to.have.deep.property('id', '2'); + expect(adUnits[0].bids[0].fpd.user.data[0].segment[3]).to.have.deep.property('id', '3'); + expect(adUnits[0].bids[0].params.user).to.have.deep.property('segments', [0, 1, 2, 3]); + + expect(adUnits[1].bids[0].fpd.user.data[0].segment[0]).to.have.deep.property('id', '1'); + expect(adUnits[1].bids[0].fpd.user.data[0].segment[1]).to.have.deep.property('id', '2'); + expect(adUnits[1].bids[0].fpd.user.data[0].segment[2]).to.have.deep.property('id', '3'); + expect(adUnits[1].bids[0].params.user).to.have.deep.property('segments', [1, 2, 3]); + + expect(adUnits[1].bids[1].fpd.user.data[0].segment[0]).to.have.deep.property('id', 'seg1'); + expect(adUnits[1].bids[1].fpd.user.data[0].segment[1]).to.have.deep.property('id', 'seg2'); + expect(adUnits[1].bids[1].fpd.user.data[0].segment[2]).to.have.deep.property('id', 'seg3'); + expect(adUnits[1].bids[1].segments[0]).to.have.deep.property('id', 'seg1'); + expect(adUnits[1].bids[1].segments[1]).to.have.deep.property('id', 'seg2'); + expect(adUnits[1].bids[1].segments[2]).to.have.deep.property('id', 'seg3'); + }); + + it('allows mapper extensions and overrides', function() { + const config = { + params: { + mapSegments: { + generic: (bid, segments) => { + bid.overrideSegments = segments; + }, + newBidder: (bid, segments) => { + bid.newBidderSegments = segments; + } + } + } + }; + + let adUnits = [ + { + bids: [ {bidder: 'newBidder'}, {bidder: 'generic'} ] + } + ]; + + const data = { + newBidder: [{id: 'nbseg1', name: 'New Bidder Segment 1'}, {id: 'nbseg2', name: 'New Bidder Segment 2'}, {id: 'nbseg3', name: 'New Bidder Segment 3'}], + generic: [{id: 'seg1'}, {id: 'seg2'}, {id: 'seg3'}] + }; + + addSegmentData(adUnits, data, config); + + expect(adUnits[0].bids[0].newBidderSegments[0]).to.have.deep.property('id', 'nbseg1'); + expect(adUnits[0].bids[0].newBidderSegments[0]).to.have.deep.property('name', 'New Bidder Segment 1'); + expect(adUnits[0].bids[0].newBidderSegments[1]).to.have.deep.property('id', 'nbseg2'); + expect(adUnits[0].bids[0].newBidderSegments[1]).to.have.deep.property('name', 'New Bidder Segment 2'); + expect(adUnits[0].bids[0].newBidderSegments[2]).to.have.deep.property('id', 'nbseg3'); + expect(adUnits[0].bids[0].newBidderSegments[2]).to.have.deep.property('name', 'New Bidder Segment 3'); + + expect(adUnits[0].bids[1].overrideSegments[0]).to.have.deep.property('id', 'seg1'); + expect(adUnits[0].bids[1].overrideSegments[1]).to.have.deep.property('id', 'seg2'); + expect(adUnits[0].bids[1].overrideSegments[2]).to.have.deep.property('id', 'seg3'); + }); + }); + + describe('Get Segments', function() { + it('gets segment data from local storage cache', function() { + const config = { + params: { + segmentCache: true, + mapSegments: { + 'generic': true + } + } + }; + + let reqBidsConfigObj = { + adUnits: [ + { + bids: [{bidder: 'generic'}] + } + ] + }; + + const data = { + audigent_segments: { + generic: [{id: 'seg1'}] + } + }; + + storage.setDataInLocalStorage(SEG_LOCAL_NAME, JSON.stringify(data)); + + getSegments(reqBidsConfigObj, () => {}, config, {}); + + expect(reqBidsConfigObj.adUnits[0].bids[0].segments[0]).to.have.deep.property('id', 'seg1'); + }); + + it('gets segment data via async request', function() { + const config = { + params: { + segmentCache: false, + mapSegments: { + 'generic': true + }, + requestParams: { + 'publisherId': 1234 + } + } + }; + + let reqBidsConfigObj = { + adUnits: [ + { + bids: [{bidder: 'generic'}] + } + ] + }; + const data = { + audigent_segments: { + generic: [{id: 'seg1'}] + } + }; + + storage.setDataInLocalStorage(HALOID_LOCAL_NAME, 'haloid'); + getSegments(reqBidsConfigObj, () => {}, config, {}); + + let request = server.requests[0]; + let postData = JSON.parse(request.requestBody); + expect(postData.config).to.have.deep.property('publisherId', 1234); + + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(reqBidsConfigObj.adUnits[0].bids[0].segments[0]).to.have.deep.property('id', 'seg1'); + }); + }); +}); diff --git a/test/spec/modules/haxmediaBidAdapter_spec.js b/test/spec/modules/haxmediaBidAdapter_spec.js new file mode 100644 index 00000000000..2e39d771bdf --- /dev/null +++ b/test/spec/modules/haxmediaBidAdapter_spec.js @@ -0,0 +1,304 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/haxmediaBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('haxmediaBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'haxmedia', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 783, + traffic: BANNER + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://balancer.haxmedia.io/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement.placementId).to.equal(783); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + }); + + it('Returns valid data for mediatype video', function () { + const playerSize = [300, 300]; + bid.mediaTypes = {}; + bid.params.traffic = VIDEO; + bid.mediaTypes[VIDEO] = { + playerSize + }; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain'); + expect(placement.traffic).to.equal(VIDEO); + expect(placement.wPlayer).to.equal(playerSize[0]); + expect(placement.hPlayer).to.equal(playerSize[1]); + }); + + it('Returns valid data for mediatype native', function () { + const native = { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + }; + + bid.mediaTypes = {}; + bid.params.traffic = NATIVE; + bid.mediaTypes[NATIVE] = native; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'native', 'schain'); + expect(placement.traffic).to.equal(NATIVE); + expect(placement.native).to.equal(native); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index ac000c1e6dd..845cf7fa010 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -1,9 +1,19 @@ +import { + id5IdSubmodule, + ID5_STORAGE_NAME, + getFromLocalStorage, + storeInLocalStorage, + expDaysStr, + nbCacheName, + getNbFromCache, + storeNbInCache +} from 'modules/id5IdSystem.js'; import { init, requestBidsHook, setSubmoduleRegistry, coreStorage } from 'modules/userId/index.js'; import { config } from 'src/config.js'; -import { id5IdSubmodule } from 'modules/id5IdSystem.js'; import { server } from 'test/mocks/xhr.js'; import events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; +import * as utils from 'src/utils.js'; let expect = require('chai').expect; @@ -11,20 +21,15 @@ describe('ID5 ID System', function() { const ID5_MODULE_NAME = 'id5Id'; const ID5_EIDS_NAME = ID5_MODULE_NAME.toLowerCase(); const ID5_SOURCE = 'id5-sync.com'; - const ID5_PARTNER = 173; - const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_PARTNER}.json`; - const ID5_COOKIE_NAME = 'id5idcookie'; - const ID5_NB_COOKIE_NAME = `id5id.1st_${ID5_PARTNER}_nb`; - const ID5_EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; + const ID5_TEST_PARTNER_ID = 173; + const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_TEST_PARTNER_ID}.json`; + const ID5_NB_STORAGE_NAME = nbCacheName(ID5_TEST_PARTNER_ID); const ID5_STORED_ID = 'storedid5id'; const ID5_STORED_SIGNATURE = '123456'; const ID5_STORED_OBJ = { 'universal_uid': ID5_STORED_ID, 'signature': ID5_STORED_SIGNATURE }; - const ID5_LEGACY_STORED_OBJ = { - 'ID5ID': ID5_STORED_ID - } const ID5_RESPONSE_ID = 'newid5id'; const ID5_RESPONSE_SIGNATURE = 'abcdef'; const ID5_JSON_RESPONSE = { @@ -33,11 +38,11 @@ describe('ID5 ID System', function() { 'link_type': 0 }; - function getId5FetchConfig(storageName = ID5_COOKIE_NAME, storageType = 'cookie') { + function getId5FetchConfig(storageName = ID5_STORAGE_NAME, storageType = 'html5') { return { name: ID5_MODULE_NAME, params: { - partner: ID5_PARTNER + partner: ID5_TEST_PARTNER_ID }, storage: { name: storageName, @@ -65,10 +70,10 @@ describe('ID5 ID System', function() { } } function getFetchCookieConfig() { - return getUserSyncConfig([getId5FetchConfig()]); + return getUserSyncConfig([getId5FetchConfig(ID5_STORAGE_NAME, 'cookie')]); } function getFetchLocalStorageConfig() { - return getUserSyncConfig([getId5FetchConfig(ID5_COOKIE_NAME, 'html5')]); + return getUserSyncConfig([getId5FetchConfig(ID5_STORAGE_NAME, 'html5')]); } function getValueConfig(value) { return getUserSyncConfig([getId5ValueConfig(value)]); @@ -82,6 +87,37 @@ describe('ID5 ID System', function() { }; } + describe('Check for valid publisher config', function() { + it('should fail with invalid config', function() { + // no config + expect(id5IdSubmodule.getId()).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ })).to.be.eq(undefined); + + // valid params, invalid storage + expect(id5IdSubmodule.getId({ params: { partner: 123 } })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: {} })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: { name: '' } })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: { type: '' } })).to.be.eq(undefined); + + // valid storage, invalid params + expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { } })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 'abc' } })).to.be.eq(undefined); + }); + + it('should warn with non-recommended storage params', function() { + let logWarnStub = sinon.stub(utils, 'logWarn'); + + id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 123 } }); + expect(logWarnStub.calledOnce).to.be.true; + logWarnStub.restore(); + + id5IdSubmodule.getId({ storage: { name: ID5_STORAGE_NAME, type: 'cookie', }, params: { partner: 123 } }); + expect(logWarnStub.calledOnce).to.be.true; + logWarnStub.restore(); + }); + }); + describe('Xhr Requests from getId()', function() { const responseHeader = { 'Content-Type': 'application/json' }; let callbackSpy = sinon.spy(); @@ -93,44 +129,49 @@ describe('ID5 ID System', function() { }); - it('should fail if no partner is provided in the config', function() { - expect(id5IdSubmodule.getId()).to.be.eq(undefined); - expect(id5IdSubmodule.getId({ })).to.be.eq(undefined); - expect(id5IdSubmodule.getId({ params: { } })).to.be.eq(undefined); - }); - - it('should call the ID5 server with 1puid field for legacy storedObj format', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_LEGACY_STORED_OBJ).callback; + it('should call the ID5 server and handle a valid response', function () { + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); expect(request.url).to.contain(ID5_ENDPOINT); expect(request.withCredentials).to.be.true; + expect(requestBody.partner).to.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.o).to.eq('pbjs'); + expect(requestBody.pd).to.eq(''); expect(requestBody.s).to.eq(''); - expect(requestBody.partner).to.eq(ID5_PARTNER); - expect(requestBody['1puid']).to.eq(ID5_STORED_ID); + expect(requestBody.provider).to.eq(''); + expect(requestBody.v).to.eq('$prebid.version$'); + expect(requestBody.gdpr).to.exist; + expect(requestBody.gdpr_consent).to.exist + expect(requestBody.us_privacy).to.exist; request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); expect(callbackSpy.calledOnce).to.be.true; expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); - it('should call the ID5 server with signature field for new storedObj format', function () { + it('should call the ID5 server with empty signature field when no stored object', function () { + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + let requestBody = JSON.parse(request.requestBody); + expect(requestBody.s).to.eq(''); + + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + }); + + it('should call the ID5 server with signature field from stored object', function () { let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE); - expect(requestBody.partner).to.eq(ID5_PARTNER); - expect(requestBody['1puid']).to.eq(''); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); it('should call the ID5 server with pd field when pd config is set', function () { @@ -144,15 +185,9 @@ describe('ID5 ID System', function() { let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; - expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE); expect(requestBody.pd).to.eq(pubData); - expect(requestBody['1puid']).to.eq(''); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); it('should call the ID5 server with empty pd field when pd config is not set', function () { @@ -164,52 +199,39 @@ describe('ID5 ID System', function() { let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; expect(requestBody.pd).to.eq(''); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); - it('should call the ID5 server with nb=1 when no stored value exists', function () { - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + it('should call the ID5 server with nb=1 when no stored value exists and reset after', function () { + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; expect(requestBody.nbPage).to.eq(1); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); }); - it('should call the ID5 server with incremented nb when stored value exists', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + it('should call the ID5 server with incremented nb when stored value exists and reset after', function () { + storeNbInCache(ID5_TEST_PARTNER_ID, 1); let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; expect(requestBody.nbPage).to.eq(2); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); }); }); @@ -218,25 +240,24 @@ describe('ID5 ID System', function() { beforeEach(function() { sinon.stub(events, 'getEvents').returns([]); - coreStorage.setCookie(ID5_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + coreStorage.removeDataFromLocalStorage(ID5_STORAGE_NAME); + coreStorage.removeDataFromLocalStorage(`${ID5_STORAGE_NAME}_last`); + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); adUnits = [getAdUnitMock()]; }); afterEach(function() { events.getEvents.restore(); - coreStorage.setCookie(ID5_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + coreStorage.removeDataFromLocalStorage(ID5_STORAGE_NAME); + coreStorage.removeDataFromLocalStorage(`${ID5_STORAGE_NAME}_last`); + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); }); - it('should add stored ID from cookie to bids', function (done) { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); + it('should add stored ID from cache to bids', function (done) { + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); setSubmoduleRegistry([id5IdSubmodule]); init(config); - config.setConfig(getFetchCookieConfig()); + config.setConfig(getFetchLocalStorageConfig()); requestBidsHook(function () { adUnits.forEach(unit => { @@ -276,43 +297,40 @@ describe('ID5 ID System', function() { }, { adUnits }); }); - it('should set nb=1 in cookie when no stored value exists', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + it('should set nb=1 in cache when no stored nb value exists and cached ID', function () { + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); setSubmoduleRegistry([id5IdSubmodule]); init(config); - config.setConfig(getFetchCookieConfig()); + config.setConfig(getFetchLocalStorageConfig()); let innerAdUnits; requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('1'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(1); }); - it('should increment nb in cookie when stored value exists', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + it('should increment nb in cache when stored nb value exists and cached ID', function () { + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + storeNbInCache(ID5_TEST_PARTNER_ID, 1); setSubmoduleRegistry([id5IdSubmodule]); init(config); - config.setConfig(getFetchCookieConfig()); + config.setConfig(getFetchLocalStorageConfig()); let innerAdUnits; requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('2'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); }); it('should call ID5 servers with signature and incremented nb post auction if refresh needed', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, (new Date(Date.now() - 50000).toUTCString()), expStr); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + storeInLocalStorage(`${ID5_STORAGE_NAME}_last`, expDaysStr(-1), 1); + storeNbInCache(ID5_TEST_PARTNER_ID, 1); - let id5Config = getFetchCookieConfig(); + let id5Config = getFetchLocalStorageConfig(); id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; setSubmoduleRegistry([id5IdSubmodule]); @@ -322,7 +340,7 @@ describe('ID5 ID System', function() { let innerAdUnits; requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('2'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); expect(server.requests).to.be.empty; events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); @@ -336,41 +354,8 @@ describe('ID5 ID System', function() { const responseHeader = { 'Content-Type': 'application/json' }; request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(coreStorage.getCookie(ID5_COOKIE_NAME)).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); - }); - - it('should call ID5 servers with 1puid and nb=1 post auction if refresh needed for legacy stored object', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_LEGACY_STORED_OBJ), expStr); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, (new Date(Date.now() - 50000).toUTCString()), expStr); - - let id5Config = getFetchCookieConfig(); - id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; - - setSubmoduleRegistry([id5IdSubmodule]); - init(config); - config.setConfig(id5Config); - - let innerAdUnits; - requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); - - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('1'); - - expect(server.requests).to.be.empty; - events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); - - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(requestBody['1puid']).to.eq(ID5_STORED_ID); - expect(requestBody.nbPage).to.eq(1); - - const responseHeader = { 'Content-Type': 'application/json' }; - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - - expect(coreStorage.getCookie(ID5_COOKIE_NAME)).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + expect(decodeURIComponent(getFromLocalStorage(ID5_STORAGE_NAME))).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); }); }); @@ -380,9 +365,6 @@ describe('ID5 ID System', function() { it('should properly decode from a stored object', function() { expect(id5IdSubmodule.decode(ID5_STORED_OBJ)).to.deep.equal(expectedDecodedObject); }); - it('should properly decode from a legacy stored object', function() { - expect(id5IdSubmodule.decode(ID5_LEGACY_STORED_OBJ)).to.deep.equal(expectedDecodedObject); - }); it('should return undefined if passed a string', function() { expect(id5IdSubmodule.decode('somestring')).to.eq(undefined); }); diff --git a/test/spec/modules/idLibrary_spec.js b/test/spec/modules/idLibrary_spec.js new file mode 100644 index 00000000000..da61850f29b --- /dev/null +++ b/test/spec/modules/idLibrary_spec.js @@ -0,0 +1,61 @@ +import * as utils from 'src/utils.js'; +import * as idlibrary from 'modules/idLibrary.js'; + +var expect = require('chai').expect; + +describe('currency', function () { + let fakeCurrencyFileServer; + let sandbox; + let clock; + + let fn = sinon.spy(); + + beforeEach(function () { + fakeCurrencyFileServer = sinon.fakeServer.create(); + sinon.stub(utils, 'logInfo'); + sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + utils.logInfo.restore(); + utils.logError.restore(); + fakeCurrencyFileServer.restore(); + idlibrary.setConfig({}); + }); + + describe('setConfig', function () { + beforeEach(function() { + sandbox = sinon.sandbox.create(); + clock = sinon.useFakeTimers(1046952000000); // 2003-03-06T12:00:00Z + }); + + afterEach(function () { + sandbox.restore(); + clock.restore(); + }); + + it('results when no config available', function () { + idlibrary.setConfig({}); + sinon.assert.called(utils.logError); + }); + it('results with config available', function () { + idlibrary.setConfig({ 'url': 'URL' }); + sinon.assert.called(utils.logInfo); + }); + it('results with config default debounce ', function () { + let config = { 'url': 'URL' } + idlibrary.setConfig(config); + expect(config.debounce).to.be.equal(250); + }); + it('results with config default fullscan ', function () { + let config = { 'url': 'URL' } + idlibrary.setConfig(config); + expect(config.fullscan).to.be.equal(true); + }); + it('results with config fullscan ', function () { + let config = { 'url': 'URL', 'fullscan': false } + idlibrary.setConfig(config); + expect(config.fullscan).to.be.equal(false); + }); + }); +}); diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 5a20944a6ed..89ec5aed8c3 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -154,6 +154,8 @@ describe('Improve Digital Adapter Tests', function () { expect(params.bid_request.version).to.equal(`${spec.version}-${idClient.CONSTANTS.CLIENT_VERSION}`); expect(params.bid_request.gdpr).to.not.exist; expect(params.bid_request.us_privacy).to.not.exist; + expect(params.bid_request.schain).to.not.exist; + expect(params.bid_request.user).to.not.exist; expect(params.bid_request.imp).to.deep.equal([ { id: '33e9500b21129f', @@ -345,6 +347,22 @@ describe('Improve Digital Adapter Tests', function () { expect(params.bid_request.schain).to.equal(schain); }); + it('should add eids', function () { + const userId = { id5id: { uid: '1111' } }; + const expectedUserObject = { ext: { eids: [{ + source: 'id5-sync.com', + uids: [{ + atype: 1, + id: '1111' + }] + }]}}; + const bidRequest = Object.assign({}, simpleBidRequest); + bidRequest.userId = userId; + const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.user).to.deep.equal(expectedUserObject); + }); + it('should return 2 requests', function () { const requests = spec.buildRequests([ simpleBidRequest, diff --git a/test/spec/modules/ironsourceBidAdapter_spec.js b/test/spec/modules/ironsourceBidAdapter_spec.js index 0c59dfef14b..93c3a6fb7b9 100644 --- a/test/spec/modules/ironsourceBidAdapter_spec.js +++ b/test/spec/modules/ironsourceBidAdapter_spec.js @@ -278,7 +278,7 @@ describe('ironsourceAdapter', function () { mediaType: VIDEO } ]; - const result = spec.interpretResponse({ body: [response] }); + const result = spec.interpretResponse({ body: response }); expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); }) diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 63b04077f4e..7ac4bd94f9d 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -18,7 +18,6 @@ describe('IndexexchangeAdapter', function () { 'sid': '00001', 'hp': 1 }, - { 'asi': 'indirectseller-2.com', 'sid': '00002', @@ -26,7 +25,74 @@ describe('IndexexchangeAdapter', function () { } ] }; - + var div_many_sizes = [ + [300, 250], + [600, 410], + [336, 280], + [400, 300], + [320, 50], + [360, 360], + [250, 250], + [320, 250], + [400, 250], + [387, 359], + [300, 50], + [372, 250], + [320, 320], + [412, 412], + [327, 272], + [312, 260], + [384, 320], + [335, 250], + [366, 305], + [374, 250], + [375, 375], + [272, 391], + [364, 303], + [414, 414], + [366, 375], + [272, 360], + [364, 373], + [366, 359], + [320, 100], + [360, 250], + [468, 60], + [480, 300], + [600, 400], + [600, 300], + [33, 28], + [40, 30], + [32, 5], + [36, 36], + [25, 25], + [320, 25], + [400, 25], + [387, 35], + [300, 5], + [372, 20], + [320, 32], + [412, 41], + [327, 27], + [312, 26], + [384, 32], + [335, 25], + [366, 30], + [374, 25], + [375, 37], + [272, 31], + [364, 303], + [414, 41], + [366, 35], + [272, 60], + [364, 73], + [366, 59], + [320, 10], + [360, 25], + [468, 6], + [480, 30], + [600, 40], + [600, 30] + ]; const DEFAULT_BANNER_VALID_BID = [ { bidder: 'ix', @@ -587,7 +653,6 @@ describe('IndexexchangeAdapter', function () { it('IX adapter reads LiveRamp IDL envelope from Prebid and adds it to Video', function () { const cloneValidBid = utils.deepClone(DEFAULT_VIDEO_VALID_BID); cloneValidBid[0].userId = utils.deepClone(DEFAULT_USERID_DATA); - const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; const payload = JSON.parse(request.data.r); @@ -972,6 +1037,97 @@ describe('IndexexchangeAdapter', function () { expect(videoImp.video.h).to.equal(DEFAULT_VIDEO_VALID_BID[0].params.size[1]); }); + it('single request under 8k size limit for large ad unit', function () { + const options = {}; + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + const requests = spec.buildRequests([bid1], options); + + const reqSize = new Blob([`${requests[0].url}?${utils.parseQueryStringParameters(requests[0].data)}`]).size; + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(1); + expect(reqSize).to.be.lessThan(8000); + }); + + it('2 requests due to 2 ad units, one larger than url size', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + bid1.params.siteId = '124'; + bid1.adUnitCode = 'div-gpt-1' + bid1.transactionId = '152e36d1-1241-4242-t35e-y1dv34d12315'; + bid1.bidId = '2f6g5s5e'; + + const requests = spec.buildRequests([bid1, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(2); + expect(requests[0].data.sn).to.be.equal(0); + expect(requests[1].data.sn).to.be.equal(1); + }); + + it('6 ad units should generate only 4 requests', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + bid1.params.siteId = '121'; + bid1.adUnitCode = 'div-gpt-1' + bid1.transactionId = 'tr1'; + bid1.bidId = '2f6g5s5e'; + + const bid2 = utils.deepClone(bid1); + bid2.transactionId = 'tr2'; + + const bid3 = utils.deepClone(bid1); + bid3.transactionId = 'tr3'; + + const bid4 = utils.deepClone(bid1); + bid4.transactionId = 'tr4'; + + const bid5 = utils.deepClone(bid1); + bid5.transactionId = 'tr5'; + + const bid6 = utils.deepClone(bid1); + bid6.transactionId = 'tr6'; + + const requests = spec.buildRequests([bid1, bid2, bid3, bid4, bid5, bid6], DEFAULT_OPTION); + + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(4); + + // check if seq number increases + for (var i = 0; i < requests.length; i++) { + const reqSize = new Blob([`${requests[i].url}?${utils.parseQueryStringParameters(requests[i].data)}`]).size; + expect(reqSize).to.be.lessThan(8000); + let payload = JSON.parse(requests[i].data.r); + if (requests.length > 1) { + expect(requests[i].data.sn).to.equal(i); + } + expect(payload.source.ext.schain).to.deep.equal(SAMPLE_SCHAIN); + } + }); + + it('multiple ad units in one request', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = [[300, 250], [300, 600], [100, 200]]; + bid1.params.siteId = '121'; + bid1.adUnitCode = 'div-gpt-1' + bid1.transactionId = 'tr1'; + bid1.bidId = '2f6g5s5e'; + + const bid2 = utils.deepClone(bid1); + bid2.transactionId = 'tr2'; + bid2.mediaTypes.banner.sizes = [[220, 221], [222, 223], [300, 250]]; + const bid3 = utils.deepClone(bid1); + bid3.transactionId = 'tr3'; + bid3.mediaTypes.banner.sizes = [[330, 331], [332, 333], [300, 250]]; + + const requests = spec.buildRequests([bid1, bid2, bid3], DEFAULT_OPTION); + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(1); + + const impressions = JSON.parse(requests[0].data.r).imp; + expect(impressions).to.be.an('array'); + expect(impressions).to.have.lengthOf(9); + }); + it('request should contain the extra banner ad sizes that IX is not configured for using the first site id in the ad unit', function () { const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); bid.sizes.push([336, 280], [970, 90]); @@ -1017,23 +1173,26 @@ describe('IndexexchangeAdapter', function () { const impressions = JSON.parse(request.data.r).imp; expect(impressions).to.be.an('array'); expect(impressions).to.have.lengthOf(4); - - expect(impressions[0].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()) - expect(impressions[1].ext.siteID).to.equal(bid.params.siteId) - expect(impressions[2].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()) - expect(impressions[3].ext.siteID).to.equal(bid.params.siteId) + expect(impressions[0].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()); + expect(impressions[1].ext.siteID).to.equal(bid.params.siteId); + expect(impressions[2].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()); + expect(impressions[3].ext.siteID).to.equal(bid.params.siteId); expect(impressions[0].banner.w).to.equal(DEFAULT_BANNER_VALID_BID[0].params.size[0]); expect(impressions[0].banner.h).to.equal(DEFAULT_BANNER_VALID_BID[0].params.size[1]); + expect(impressions[1].banner.w).to.equal(bid.params.size[0]); expect(impressions[1].banner.h).to.equal(bid.params.size[1]); + expect(impressions[2].banner.w).to.equal(DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][0]); expect(impressions[2].banner.h).to.equal(DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][1]); + expect(impressions[3].banner.w).to.equal(bid.mediaTypes.banner.sizes[1][0]); expect(impressions[3].banner.h).to.equal(bid.mediaTypes.banner.sizes[1][1]); expect(impressions[0].ext.sid).to.equal(`${DEFAULT_BANNER_VALID_BID[0].params.size[0].toString()}x${DEFAULT_BANNER_VALID_BID[0].params.size[1].toString()}`); expect(impressions[1].ext.sid).to.equal(`${bid.params.size[0].toString()}x${bid.params.size[1].toString()}`); + expect(impressions[2].ext.sid).to.equal(`${DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][0].toString()}x${DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][1].toString()}`); expect(impressions[3].ext.sid).to.equal(`${bid.mediaTypes.banner.sizes[1][0].toString()}x${bid.mediaTypes.banner.sizes[1][1].toString()}`); }); @@ -1045,6 +1204,28 @@ describe('IndexexchangeAdapter', function () { expect(impressions).to.be.an('array'); expect(impressions).to.have.lengthOf(1); }); + + describe('detect missing sizes', function () { + beforeEach(function () { + config.setConfig({ + ix: { + detectMissingSizes: false + } + }); + }) + + it('request should not contain missing sizes if detectMissingSizes = false', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + + const requests = spec.buildRequests([bid1, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + + const impressions = JSON.parse(requests[0].data.r).imp; + + expect(impressions).to.be.an('array'); + expect(impressions).to.have.lengthOf(2); + }); + }); }); describe('buildRequestVideo', function () { diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js index b5bacdc3694..48b432b6bb4 100644 --- a/test/spec/modules/jwplayerRtdProvider_spec.js +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -1,4 +1,5 @@ -import { fetchTargetingForMediaId, getTargetingForBid, +import { fetchTargetingForMediaId, getVatFromCache, extractPublisherParams, + formatTargetingResponse, getVatFromPlayer, enrichAdUnits, addTargetingToBid, fetchTargetingInformation, jwplayerSubmodule } from 'modules/jwplayerRtdProvider.js'; import { server } from 'test/mocks/xhr.js'; @@ -39,11 +40,7 @@ describe('jwplayerRtdProvider', function() { }) ); - const targetingInfo = getTargetingForBid({ - jwTargeting: { - mediaID: testIdForSuccess - } - }); + const targetingInfo = getVatFromCache(testIdForSuccess); const validTargeting = { segments: validSegments, @@ -62,21 +59,13 @@ describe('jwplayerRtdProvider', function() { it('should not write to cache when response is malformed', function() { request.respond('{]'); - const targetingInfo = getTargetingForBid({ - jwTargeting: { - mediaID: testIdForFailure - } - }); + const targetingInfo = getVatFromCache(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 - } - }); + const targetingInfo = getVatFromCache(testIdForFailure); expect(targetingInfo).to.be.null; }); @@ -92,27 +81,46 @@ describe('jwplayerRtdProvider', function() { ] }) ); - const targetingInfo = getTargetingForBid({ - jwTargeting: { - mediaID: testIdForFailure - } - }); + const targetingInfo = getVatFromCache(testIdForFailure); expect(targetingInfo).to.be.null; }); it('should not write to cache when request errors', function() { request.error(); - const targetingInfo = getTargetingForBid({ - jwTargeting: { - mediaID: testIdForFailure - } - }); + const targetingInfo = getVatFromCache(testIdForFailure); expect(targetingInfo).to.be.null; }); }); }); - describe('Get targeting for bid', function() { + describe('Format targeting response', function () { + it('should exclude segment key when absent', function () { + const targeting = formatTargetingResponse({ mediaID: 'test' }); + expect(targeting).to.not.have.property('segments'); + }); + + it('should exclude content block when mediaId is absent', function () { + const targeting = formatTargetingResponse({ segments: ['test'] }); + expect(targeting).to.not.have.property('content'); + }); + + it('should return proper format', function () { + const segments = ['123']; + const mediaID = 'test'; + const expectedContentId = 'jw_' + mediaID; + const expectedContent = { + id: expectedContentId + }; + const targeting = formatTargetingResponse({ + segments, + mediaID + }); + expect(targeting).to.have.deep.property('segments', segments); + expect(targeting).to.have.deep.property('content', expectedContent); + }); + }); + + describe('Get VAT from player', function () { const mediaIdWithSegment = 'media_ID_1'; const mediaIdNoSegment = 'media_ID_2'; const mediaIdForCurrentItem = 'media_ID_current'; @@ -121,18 +129,8 @@ describe('jwplayerRtdProvider', function() { 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 - } - }); + const targeting = getVatFromPlayer(invalidPlayerID, mediaIdNotCached); expect(targeting).to.be.null; }); @@ -184,81 +182,358 @@ describe('jwplayerRtdProvider', function() { }); it('returns null when player ID does not match player on page', function () { - const targeting = getTargetingForBid({ - jwTargeting: { - playerID: invalidPlayerID, - mediaID: mediaIdNotCached - } - }); + const targeting = getVatFromPlayer(invalidPlayerID, 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 - } - }); + const targeting = getVatFromPlayer(validPlayerID, 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('caches segments when media ID matches a playist item with segments', function () { + getVatFromPlayer(validPlayerID, mediaIdWithSegment); + const vat = getVatFromCache(mediaIdWithSegment); + expect(vat.segments).to.deep.equal(validSegments); }); it('returns segments of current item when media ID is missing', function () { - const targeting = getTargetingForBid({ - jwTargeting: { - playerID: validPlayerID - } - }); + const targeting = getVatFromPlayer(validPlayerID); expect(targeting).to.deep.equal(targetingForCurrentItem); }); it('caches segments from the current item', function () { - getTargetingForBid({ - jwTargeting: { - playerID: validPlayerID - } - }); + getVatFromPlayer(validPlayerID); window.jwplayer = null; - const targeting2 = getTargetingForBid({ - jwTargeting: { - playerID: invalidPlayerID, - mediaID: mediaIdForCurrentItem - } - }); - expect(targeting2).to.deep.equal(targetingForCurrentItem); + const targeting = getVatFromCache(mediaIdForCurrentItem); + expect(targeting).to.deep.equal(targetingForCurrentItem); }); it('returns undefined segments when segments are absent', function () { - const targeting = getTargetingForBid({ - jwTargeting: { - playerID: validPlayerID, - mediaID: mediaIdNoSegment - } - }); + const targeting = getVatFromPlayer(validPlayerID, mediaIdNoSegment); expect(targeting).to.deep.equal({ mediaID: mediaIdNoSegment, segments: undefined }); }); + + describe('Get Bid Request Data', function () { + it('executes immediately while request is active if player has item', function () { + const bidRequestSpy = sinon.spy(); + const fakeServer = sinon.createFakeServer(); + fakeServer.respondImmediately = false; + fakeServer.autoRespond = false; + + fetchTargetingForMediaId(mediaIdWithSegment); + + const bid = {}; + const adUnit = { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: mediaIdWithSegment, + playerID: validPlayerID + } + } + } + }, + bids: [ + bid + ] + }; + const expectedContentId = 'jw_' + mediaIdWithSegment; + const expectedTargeting = { + segments: validSegments, + content: { + id: expectedContentId + } + }; + jwplayerSubmodule.getBidRequestData({ adUnits: [adUnit] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); + fakeServer.respond(); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + }); + }); + }); + + describe('Enrich ad units', function () { + const contentIdForSuccess = 'jw_' + testIdForSuccess; + const expectedTargetingForSuccess = { + segments: validSegments, + content: { + id: contentIdForSuccess + } + }; + 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('adds targeting when pending request succeeds', function () { + fetchTargetingForMediaId(testIdForSuccess); + const bids = [ + { + id: 'bid1' + }, + { + id: 'bid2' + } + ]; + const adUnit = { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: testIdForSuccess + } + } + } + }, + bids + }; + + enrichAdUnits([adUnit]); + const bid1 = bids[0]; + const bid2 = bids[1]; + expect(bid1).to.not.have.property('rtd'); + expect(bid2).to.not.have.property('rtd'); + + const request = fakeServer.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4', + jwpseg: validSegments + } + ] + }) + ); + + expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + }); + + it('immediately adds cached targeting', function () { + fetchTargetingForMediaId(testIdForSuccess); + const bids = [ + { + id: 'bid1' + }, + { + id: 'bid2' + } + ]; + const adUnit = { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: testIdForSuccess + } + } + } + }, + bids + }; + const request = fakeServer.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4', + jwpseg: validSegments + } + ] + }) + ); + + enrichAdUnits([adUnit]); + const bid1 = bids[0]; + const bid2 = bids[1]; + expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + }); + + it('adds content block when segments are absent and no request is pending', function () { + const expectedTargetingForFailure = { + content: { + id: 'jw_' + testIdForFailure + } + }; + const bids = [ + { + id: 'bid1' + }, + { + id: 'bid2' + } + ]; + const adUnit = { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: testIdForFailure + } + } + } + }, + bids + }; + + enrichAdUnits([adUnit]); + const bid1 = bids[0]; + const bid2 = bids[1]; + expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForFailure); + expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForFailure); + }); + }); + + describe(' Extract Publisher Params', function () { + it('should default to config', function () { + const config = { mediaID: 'test' }; + + const adUnit1 = { fpd: { context: {} } }; + const targeting1 = extractPublisherParams(adUnit1, config); + expect(targeting1).to.deep.equal(config); + + const adUnit2 = { fpd: { context: { data: { jwTargeting: {} } } } }; + const targeting2 = extractPublisherParams(adUnit2, config); + expect(targeting2).to.deep.equal(config); + + const targeting3 = extractPublisherParams(null, config); + expect(targeting3).to.deep.equal(config); + }); + + it('should prioritize adUnit properties ', function () { + const expectedMediaID = 'test_media_id'; + const expectedPlayerID = 'test_player_id'; + const config = { playerID: 'bad_id', mediaID: 'bad_id' }; + + const adUnit = { fpd: { context: { data: { jwTargeting: { mediaID: expectedMediaID, playerID: expectedPlayerID } } } } }; + const targeting = extractPublisherParams(adUnit, config); + expect(targeting).to.have.property('mediaID', expectedMediaID); + expect(targeting).to.have.property('playerID', expectedPlayerID); + }); + + it('should use config properties as fallbacks', function () { + const expectedMediaID = 'test_media_id'; + const expectedPlayerID = 'test_player_id'; + const config = { playerID: expectedPlayerID, mediaID: 'bad_id' }; + + const adUnit = { fpd: { context: { data: { jwTargeting: { mediaID: expectedMediaID } } } } }; + const targeting = extractPublisherParams(adUnit, config); + expect(targeting).to.have.property('mediaID', expectedMediaID); + expect(targeting).to.have.property('playerID', expectedPlayerID); + }); + + it('should return empty object when Publisher Params are absent', function () { + const targeting = extractPublisherParams(null, null); + expect(targeting).to.deep.equal({}); + }) + }); + + describe('Add Targeting to Bid', function () { + const targeting = {foo: 'bar'}; + + it('creates realTimeData when absent from Bid', function () { + const targeting = {foo: 'bar'}; + const bid = {}; + addTargetingToBid(bid, targeting); + expect(bid).to.have.property('rtd'); + expect(bid).to.have.nested.property('rtd.jwplayer.targeting', targeting); + }); + + it('adds to existing realTimeData', function () { + const otherRtd = { + targeting: { + seg: 'rtd seg' + } + }; + + const bid = { + rtd: { + otherRtd + } + }; + + addTargetingToBid(bid, targeting); + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); + + expect(rtd).to.have.deep.property('otherRtd', otherRtd); + }); + + it('adds to existing realTimeData.jwplayer', function () { + const otherInfo = { seg: 'rtd seg' }; + const bid = { + rtd: { + jwplayer: { + otherInfo + } + } + }; + addTargetingToBid(bid, targeting); + + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.otherInfo', otherInfo); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); + }); + + it('overrides existing jwplayer.targeting', function () { + const otherInfo = { seg: 'rtd seg' }; + const bid = { + rtd: { + jwplayer: { + targeting: { + otherInfo + } + } + } + }; + addTargetingToBid(bid, targeting); + + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); + }); + + it('creates jwplayer when absent from realTimeData', function () { + const bid = { rtd: {} }; + addTargetingToBid(bid, targeting); + + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); }); }); @@ -267,13 +542,47 @@ describe('jwplayerRtdProvider', function() { expect(jwplayerSubmodule.init()).to.equal(true); }); - describe('getData', function () { + describe('Get Bid Request Data', function () { const validMediaIDs = ['media_ID_1', 'media_ID_2', 'media_ID_3']; let bidRequestSpy; let fakeServer; let clock; + let bidReqConfig; beforeEach(function () { + bidReqConfig = { + adUnits: [ + { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: validMediaIDs[0] + } + } + } + }, + bids: [ + {}, {} + ] + }, + { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: validMediaIDs[1] + } + } + } + }, + bids: [ + {}, {} + ] + } + ] + }; + bidRequestSpy = sinon.spy(); fakeServer = sinon.createFakeServer(); @@ -288,19 +597,24 @@ describe('jwplayerRtdProvider', function() { fakeServer.respond(); }); + it('executes callback immediately when ad units are missing', function () { + jwplayerSubmodule.getBidRequestData({ adUnits: [] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + it('executes callback immediately when no requests are pending', function () { fetchTargetingInformation({ mediaIDs: [] }); - jwplayerSubmodule.getData([], bidRequestSpy); + jwplayerSubmodule.getBidRequestData(bidReqConfig, bidRequestSpy); expect(bidRequestSpy.calledOnce).to.be.true; }); - it('executes callback after requests complete', function() { + it('executes callback only after requests in adUnit complete', function() { fetchTargetingInformation({ mediaIDs: validMediaIDs }); - jwplayerSubmodule.getData([], bidRequestSpy); + jwplayerSubmodule.getBidRequestData(bidReqConfig, bidRequestSpy); expect(bidRequestSpy.notCalled).to.be.true; const req1 = fakeServer.requests[0]; @@ -311,88 +625,102 @@ describe('jwplayerRtdProvider', function() { 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(); + req3.respond(); expect(bidRequestSpy.calledOnce).to.be.true; }); - it('returns data in proper structure', function () { - const adUnitCode = 'test_ad_unit'; + it('sets targeting data in proper structure', function () { + const bid = {}; const adUnitWithMediaId = { - code: adUnitCode, - jwTargeting: { - mediaID: testIdForSuccess - } + fpd: { + context: { + data: { + jwTargeting: { + mediaID: testIdForSuccess + } + } + } + }, + bids: [ + bid + ] }; const adUnitEmpty = { code: 'test_ad_unit_empty' }; - const expectedData = {}; const expectedContentId = 'jw_' + testIdForSuccess; - expectedData[adUnitCode] = { - jwTargeting: { - segments: validSegments, - content: { - id: expectedContentId - } + const expectedTargeting = { + segments: validSegments, + content: { + id: expectedContentId } }; - jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); - expect(bidRequestSpy.calledOnceWithExactly(expectedData)).to.be.true; + jwplayerSubmodule.getBidRequestData({ adUnits: [adUnitWithMediaId, adUnitEmpty] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); }); - it('returns an empty object when media id is invalid', function () { + it('excludes segments when absent', function () { const adUnitCode = 'test_ad_unit'; - const adUnitWithMediaId = { - code: adUnitCode, - jwTargeting: { - mediaID: testIdForFailure - } + const bid = {}; + const adUnit = { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: testIdForFailure + } + } + } + }, + bids: [ bid ] }; - const adUnitEmpty = { - code: 'test_ad_unit_empty' + const expectedContentId = 'jw_' + adUnit.fpd.context.data.jwTargeting.mediaID; + const expectedTargeting = { + content: { + id: expectedContentId + } }; - jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); - expect(bidRequestSpy.calledOnceWithExactly({})).to.be.true; + jwplayerSubmodule.getBidRequestData({ adUnits: [ adUnit ] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid.rtd.jwplayer.targeting).to.not.have.property('segments'); + expect(bid.rtd.jwplayer.targeting).to.not.have.property('segments'); + expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); }); - it('returns an empty object when jwTargeting block is absent', function () { + it('does not modify bid when jwTargeting block is absent', function () { const adUnitCode = 'test_ad_unit'; + const bid1 = {}; + const bid2 = {}; + const bid3 = {}; const adUnitWithMediaId = { code: adUnitCode, - mediaID: testIdForSuccess + mediaID: testIdForSuccess, + bids: [ bid1 ] }; const adUnitEmpty = { - code: 'test_ad_unit_empty' + code: 'test_ad_unit_empty', + bids: [ bid2 ] }; - jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); - expect(bidRequestSpy.calledOnceWithExactly({})).to.be.true; + const adUnitEmptyfpd = { + code: 'test_ad_unit_empty_fpd', + fpd: { + context: { + id: 'sthg' + } + }, + bids: [ bid3 ] + }; + + jwplayerSubmodule.getBidRequestData({ adUnits: [adUnitWithMediaId, adUnitEmpty, adUnitEmptyfpd] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid1).to.not.have.property('rtd'); + expect(bid2).to.not.have.property('rtd'); + expect(bid3).to.not.have.property('rtd'); }); }); }); diff --git a/test/spec/modules/krushmediaBidAdapter_spec.js b/test/spec/modules/krushmediaBidAdapter_spec.js new file mode 100644 index 00000000000..3af9ed64c43 --- /dev/null +++ b/test/spec/modules/krushmediaBidAdapter_spec.js @@ -0,0 +1,329 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/krushmediaBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('KrushmediabBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'krushmedia', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + key: 783, + traffic: BANNER + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.key; + 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://ads4.krushmedia.com/?c=rtb&m=hb'); + }); + 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'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('key', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement.key).to.equal(783); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + }); + + it('Returns valid data for mediatype video', function () { + const playerSize = [300, 300]; + bid.mediaTypes = {}; + bid.params.traffic = VIDEO; + bid.mediaTypes[VIDEO] = { + playerSize + }; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('key', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain'); + expect(placement.traffic).to.equal(VIDEO); + expect(placement.wPlayer).to.equal(playerSize[0]); + expect(placement.hPlayer).to.equal(playerSize[1]); + }); + + it('Returns valid data for mediatype native', function () { + const native = { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + }; + + bid.mediaTypes = {}; + bid.params.traffic = NATIVE; + bid.mediaTypes[NATIVE] = native; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('key', 'bidId', 'traffic', 'native', 'schain'); + expect(placement.traffic).to.equal(NATIVE); + expect(placement.native).to.equal(native); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.krushmedia.com/html?src=pbjs&gdpr=1&gdpr_consent=ALL') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1NNN' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.krushmedia.com/html?src=pbjs&ccpa_consent=1NNN') + }); + }); +}); diff --git a/test/spec/modules/lemmaBidAdapter_spec.js b/test/spec/modules/lemmaBidAdapter_spec.js index a236ac17d71..a00c25d126c 100644 --- a/test/spec/modules/lemmaBidAdapter_spec.js +++ b/test/spec/modules/lemmaBidAdapter_spec.js @@ -331,5 +331,39 @@ describe('lemmaBidAdapter', function() { }); }); }); + describe('getUserSyncs', function() { + const syncurl_iframe = 'https://sync.lemmatechnologies.com/js/usersync.html?pid=1001'; + let sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + afterEach(function() { + sandbox.restore(); + }); + + it('execute as per config', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + }]); + }); + + it('CCPA/USP', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&us_privacy=1NYN` + }]); + }); + + it('GDPR', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: false, consentString: 'foo' }, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=0&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: undefined }, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=` + }]); + }); + }); }); }); diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index 80f776168c4..aae60cbcd19 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,46 +1,50 @@ -import {liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage} from 'modules/liveIntentIdSystem.js'; +import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; import * as utils from 'src/utils.js'; -import {uspDataHandler} from '../../../src/adapterManager.js'; -import {server} from 'test/mocks/xhr.js'; +import { gdprDataHandler, uspDataHandler } from '../../../src/adapterManager.js'; +import { server } from 'test/mocks/xhr.js'; const PUBLISHER_ID = '89899'; const defaultConfigParams = { params: {publisherId: PUBLISHER_ID} }; const responseHeader = {'Content-Type': 'application/json'} -describe('LiveIntentId', function () { - let pixel = {}; +describe('LiveIntentId', function() { let logErrorStub; - let consentDataStub; + let uspConsentDataStub; + let gdprConsentDataStub; let getCookieStub; let getDataFromLocalStorageStub; let imgStub; - beforeEach(function () { - imgStub = sinon.stub(window, 'Image').returns(pixel); + beforeEach(function() { + imgStub = sinon.stub(utils, 'triggerPixel'); getCookieStub = sinon.stub(storage, 'getCookie'); getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); logErrorStub = sinon.stub(utils, 'logError'); - consentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); }); - afterEach(function () { - pixel = {}; + afterEach(function() { imgStub.restore(); getCookieStub.restore(); getDataFromLocalStorageStub.restore(); logErrorStub.restore(); - consentDataStub.restore(); + uspConsentDataStub.restore(); + gdprConsentDataStub.restore(); resetLiveIntentIdSubmodule(); }); - it('should initialize LiveConnect with a us privacy string when getId, and include it in all requests', function () { - consentDataStub.returns('1YNY'); + it('should initialize LiveConnect with a privacy string when getId, and include it in the resolution request', function() { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: true, + consentString: 'consentDataString' + }) let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; - expect(pixel.src).to.match(/.*us_privacy=1YNY/); submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.match(/.*us_privacy=1YNY/); + let request = server.requests[1]; + expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&gdpr_consent=consentDataString.*/); request.respond( 200, responseHeader, @@ -49,9 +53,22 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should fire an event when getId', function () { + it('should fire an event when getId', function() { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: true, + consentString: 'consentDataString' + }) liveIntentIdSubmodule.getId(defaultConfigParams); - expect(pixel.src).to.match(/https:\/\/rp.liadm.com\/p\?wpn=prebid.*/) + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?wpn=prebid.*us_privacy=1YNY.*&gdpr=1&gdpr_consent=consentDataString.*/); + }); + + it('should fire an event when getId and a hash is provided', function() { + liveIntentIdSubmodule.getId({ params: { + ...defaultConfigParams, + emailHash: '58131bc547fb87af94cebdaf3102321f' + }}); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/) }); it('should initialize LiveConnect with the config params when decode and emit an event', function () { @@ -64,40 +81,52 @@ describe('LiveIntentId', function () { collectorUrl: 'https://collector.liveintent.com' } } - } }); - expect(pixel.src).to.match(/https:\/\/collector.liveintent.com\/p\?aid=a-0001&wpn=prebid.*/) + }}); + expect(server.requests[0].url).to.match(/https:\/\/collector.liveintent.com\/j\?aid=a-0001&wpn=prebid.*/); }); - it('should initialize LiveConnect and emit an event with a us privacy string when decode', function () { - consentDataStub.returns('1YNY'); + it('should initialize LiveConnect and emit an event with a privacy string when decode', function() { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: false, + consentString: 'consentDataString' + }) liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(pixel.src).to.match(/.*us_privacy=1YNY/); + expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*/); }); - it('should not return a decoded identifier when the unifiedId is not present in the value', function () { - const result = liveIntentIdSubmodule.decode({additionalData: 'data'}); + it('should fire an event when decode and a hash is provided', function() { + liveIntentIdSubmodule.decode({}, { params: { + ...defaultConfigParams.params, + emailHash: '58131bc547fb87af94cebdaf3102321f' + }}); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/); + }); + + it('should not return a decoded identifier when the unifiedId is not present in the value', function() { + const result = liveIntentIdSubmodule.decode({ additionalData: 'data' }); expect(result).to.be.undefined; }); - it('should fire an event when decode', function () { + it('should fire an event when decode', function() { liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(pixel.src).to.be.not.null + expect(server.requests[0].url).to.be.not.null }); - it('should initialize LiveConnect and send data only once', function () { + it('should initialize LiveConnect and send data only once', function() { liveIntentIdSubmodule.getId(defaultConfigParams); liveIntentIdSubmodule.decode({}, defaultConfigParams); liveIntentIdSubmodule.getId(defaultConfigParams); liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(imgStub.calledOnce).to.be.true; + expect(server.requests.length).to.be.eq(1); }); - it('should call the Custom URL of the LiveIntent Identity Exchange endpoint', function () { + it('should call the Custom URL of the LiveIntent Identity Exchange endpoint', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams.params, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899'); request.respond( 200, @@ -107,7 +136,7 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the default url of the LiveIntent Identity Exchange endpoint, with a partner', function () { + it('should call the default url of the LiveIntent Identity Exchange endpoint, with a partner', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { @@ -118,7 +147,7 @@ describe('LiveIntentId', function () { } } }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899'); request.respond( 200, @@ -128,12 +157,12 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the LiveIntent Identity Exchange endpoint, with no additional query params', function () { + it('should call the LiveIntent Identity Exchange endpoint, with no additional query params', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899'); request.respond( 200, @@ -143,12 +172,12 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should log an error and continue to callback if ajax request errors', function () { + it('should log an error and continue to callback if ajax request errors', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899'); request.respond( 503, @@ -159,13 +188,13 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should include the LiveConnect identifier when calling the LiveIntent Identity Exchange endpoint', function () { + it('should include the LiveConnect identifier when calling the LiveIntent Identity Exchange endpoint', function() { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}`); request.respond( 200, @@ -175,7 +204,7 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should include the LiveConnect identifier and additional Identifiers to resolve', function () { + it('should include the LiveConnect identifier and additional Identifiers to resolve', function() { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); getDataFromLocalStorageStub.withArgs('_thirdPC').returns('third-pc'); @@ -188,7 +217,7 @@ describe('LiveIntentId', function () { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&_thirdPC=third-pc`); request.respond( 200, @@ -198,7 +227,7 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should include an additional identifier value to resolve even if it is an object', function () { + it('should include an additional identifier value to resolve even if it is an object', function() { getCookieStub.returns(null); getDataFromLocalStorageStub.withArgs('_thirdPC').returns({'key': 'value'}); const configParams = { params: { @@ -210,7 +239,7 @@ describe('LiveIntentId', function () { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?_thirdPC=%7B%22key%22%3A%22value%22%7D'); request.respond( 200, @@ -219,4 +248,10 @@ describe('LiveIntentId', function () { ); expect(callBackSpy.calledOnce).to.be.true; }); + + it('should send an error when the cookie jar throws an unexpected error', function() { + getCookieStub.throws('CookieError', 'A message'); + liveIntentIdSubmodule.getId(defaultConfigParams); + expect(imgStub.getCall(0).args[0]).to.match(/.*ae=.+/); + }); }); diff --git a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js index c723f589fa0..ba9430e0b95 100644 --- a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js +++ b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js @@ -121,6 +121,7 @@ const MOCK = { const ANALYTICS_MESSAGE = { publisherId: 'CC411485-42BC-4F92-8389-42C503EE38D7', gdpr: [{}], + auctionIds: ['25c6d7f5-699a-4bfc-87c9-996f915341fa'], bidAdUnits: [ { adUnit: 'panorama_d_1', @@ -136,19 +137,22 @@ const ANALYTICS_MESSAGE = { adUnit: 'panorama_d_1', bidder: 'livewrapped', timeStamp: 1519149562216, - gdpr: 0 + gdpr: 0, + auctionId: 0 }, { adUnit: 'box_d_1', bidder: 'livewrapped', timeStamp: 1519149562216, - gdpr: 0 + gdpr: 0, + auctionId: 0 }, { adUnit: 'box_d_2', bidder: 'livewrapped', timeStamp: 1519149562216, - gdpr: 0 + gdpr: 0, + auctionId: 0 } ], responses: [ @@ -161,7 +165,9 @@ const ANALYTICS_MESSAGE = { cpm: 1.1, ttr: 200, IsBid: true, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 }, { timeStamp: 1519149562216, @@ -172,14 +178,18 @@ const ANALYTICS_MESSAGE = { cpm: 2.2, ttr: 300, IsBid: true, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 }, { timeStamp: 1519149562216, adUnit: 'box_d_2', bidder: 'livewrapped', ttr: 200, - IsBid: false + IsBid: false, + gdpr: 0, + auctionId: 0 } ], timeouts: [], @@ -191,7 +201,9 @@ const ANALYTICS_MESSAGE = { width: 980, height: 240, cpm: 1.1, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 }, { timeStamp: 1519149562216, @@ -200,7 +212,9 @@ const ANALYTICS_MESSAGE = { width: 300, height: 250, cpm: 2.2, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 } ] }; @@ -351,7 +365,9 @@ describe('Livewrapped analytics adapter', function () { } }, ); - events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_WON, MOCK.BID_WON[0]); events.emit(AUCTION_END, MOCK.AUCTION_END); clock.tick(BID_WON_TIMEOUT + 1000); @@ -366,6 +382,104 @@ describe('Livewrapped analytics adapter', function () { expect(message.requests.length).to.equal(2); expect(message.requests[0].gdpr).to.equal(0); expect(message.requests[1].gdpr).to.equal(0); + + expect(message.responses.length).to.equal(1); + expect(message.responses[0].gdpr).to.equal(0); + + expect(message.wins.length).to.equal(1); + expect(message.wins[0].gdpr).to.equal(0); + }); + + it('should forward floor data', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, { + 'bidder': 'livewrapped', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'livewrapped', + 'adUnitCode': 'panorama_d_1', + 'bidId': '2ecff0db240757', + 'floorData': { + 'floorValue': 1.1, + 'floorCurrency': 'SEK' + } + } + ], + 'start': 1519149562216 + }); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.gdpr.length).to.equal(1); + + expect(message.responses.length).to.equal(1); + expect(message.responses[0].floor).to.equal(1.1); + expect(message.responses[0].floorCur).to.equal('SEK'); + + expect(message.wins.length).to.equal(1); + expect(message.wins[0].floor).to.equal(1.1); + expect(message.wins[0].floorCur).to.equal('SEK'); + }); + + it('should forward Livewrapped floor data', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, { + 'bidder': 'livewrapped', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'livewrapped', + 'adUnitCode': 'panorama_d_1', + 'bidId': '2ecff0db240757', + 'lwflr': { + 'flr': 1.1 + } + }, + { + 'bidder': 'livewrapped', + 'adUnitCode': 'box_d_1', + 'bidId': '3ecff0db240757', + 'lwflr': { + 'flr': 1.1, + 'bflrs': {'livewrapped': 2.2} + } + } + ], + 'start': 1519149562216 + }); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.gdpr.length).to.equal(1); + + expect(message.responses.length).to.equal(2); + expect(message.responses[0].floor).to.equal(1.1); + expect(message.responses[1].floor).to.equal(2.2); + + expect(message.wins.length).to.equal(2); + expect(message.wins[0].floor).to.equal(1.1); + expect(message.wins[1].floor).to.equal(2.2); }); }); diff --git a/test/spec/modules/livewrappedBidAdapter_spec.js b/test/spec/modules/livewrappedBidAdapter_spec.js index 2d5ba3f48df..7983e8fbb0b 100644 --- a/test/spec/modules/livewrappedBidAdapter_spec.js +++ b/test/spec/modules/livewrappedBidAdapter_spec.js @@ -811,7 +811,7 @@ describe('Livewrapped adapter tests', function () { let data = JSON.parse(result.data); expect(data.rtbData.user.ext.eids).to.deep.equal([{ - 'source': 'pubcommon', + 'source': 'pubcid.org', 'uids': [{ 'id': 'publisher-common-id', 'atype': 1 @@ -819,6 +819,25 @@ describe('Livewrapped adapter tests', function () { }]); }); + it('should make use of criteoId if available', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + let testbidRequest = clone(bidderRequest); + delete testbidRequest.bids[0].params.userId; + testbidRequest.bids[0].userId = {}; + testbidRequest.bids[0].userId.criteoId = 'criteo-id'; + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(data.rtbData.user.ext.eids).to.deep.equal([{ + 'source': 'criteo.com', + 'uids': [{ + 'id': 'criteo-id', + 'atype': 1 + }] + }]); + }); + it('should send schain object if available', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); diff --git a/test/spec/modules/lkqdBidAdapter_spec.js b/test/spec/modules/lkqdBidAdapter_spec.js index f10d936a28b..1cd33d9ec59 100644 --- a/test/spec/modules/lkqdBidAdapter_spec.js +++ b/test/spec/modules/lkqdBidAdapter_spec.js @@ -1,6 +1,7 @@ import { spec } from 'modules/lkqdBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -const { expect } = require('chai'); +import { config } from 'src/config.js'; +import { expect } from 'chai'; describe('LKQD Bid Adapter Test', () => { const adapter = newBidder(spec); @@ -47,7 +48,9 @@ describe('LKQD Bid Adapter Test', () => { 'bidder': 'lkqd', 'params': { 'siteId': '662921', - 'placementId': '263' + 'placementId': '263', + 'c1': 'newWindow', + 'c20': 'lkqdCustom' }, 'adUnitCode': 'lkqd', 'sizes': [[300, 250], [640, 480]], @@ -75,6 +78,10 @@ describe('LKQD Bid Adapter Test', () => { ]; it('should populate available parameters', () => { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + const requests = spec.buildRequests(bidRequests); expect(requests.length).to.equal(2); const r1 = requests[0].data; @@ -82,14 +89,26 @@ describe('LKQD Bid Adapter Test', () => { expect(r1).to.have.string('&sid=662921&'); expect(r1).to.have.string('&width=300&'); expect(r1).to.have.string('&height=250&'); + expect(r1).to.have.string('&coppa=1&'); + expect(r1).to.have.string('&c1=newWindow&'); + expect(r1).to.have.string('&c20=lkqdCustom'); const r2 = requests[1].data; expect(r2).to.have.string('pid=263&'); expect(r2).to.have.string('&sid=662921&'); expect(r2).to.have.string('&width=640&'); expect(r2).to.have.string('&height=480&'); + expect(r2).to.have.string('&coppa=1&'); + expect(r2).to.have.string('&c1=newWindow&'); + expect(r2).to.have.string('&c20=lkqdCustom'); + + config.getConfig.restore(); }); it('should not populate unspecified parameters', () => { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(false); + const requests = spec.buildRequests(bidRequests); expect(requests.length).to.equal(2); const r1 = requests[0].data; @@ -99,6 +118,8 @@ describe('LKQD Bid Adapter Test', () => { expect(r1).to.not.have.string('&contentlength='); expect(r1).to.not.have.string('&contenturl='); expect(r1).to.not.have.string('&schain='); + expect(r1).to.not.have.string('&c10='); + expect(r1).to.not.have.string('coppa'); const r2 = requests[1].data; expect(r2).to.not.have.string('&dnt='); expect(r2).to.not.have.string('&contentid='); @@ -106,6 +127,10 @@ describe('LKQD Bid Adapter Test', () => { expect(r2).to.not.have.string('&contentlength='); expect(r2).to.not.have.string('&contenturl='); expect(r2).to.not.have.string('&schain='); + expect(r2).to.not.have.string('&c39='); + expect(r2).to.not.have.string('coppa'); + + config.getConfig.restore(); }); it('should handle single size request', () => { diff --git a/test/spec/modules/lunamediahbBidAdapter_spec.js b/test/spec/modules/lunamediahbBidAdapter_spec.js new file mode 100644 index 00000000000..e9f88935ed5 --- /dev/null +++ b/test/spec/modules/lunamediahbBidAdapter_spec.js @@ -0,0 +1,304 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/lunamediahbBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('LunamediaHBBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'lunamediahb', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 783, + traffic: BANNER + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://balancer.lmgssp.com/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement.placementId).to.equal(783); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + }); + + it('Returns valid data for mediatype video', function () { + const playerSize = [300, 300]; + bid.mediaTypes = {}; + bid.params.traffic = VIDEO; + bid.mediaTypes[VIDEO] = { + playerSize + }; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain'); + expect(placement.traffic).to.equal(VIDEO); + expect(placement.wPlayer).to.equal(playerSize[0]); + expect(placement.hPlayer).to.equal(playerSize[1]); + }); + + it('Returns valid data for mediatype native', function () { + const native = { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + }; + + bid.mediaTypes = {}; + bid.params.traffic = NATIVE; + bid.mediaTypes[NATIVE] = native; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'native', 'schain'); + expect(placement.traffic).to.equal(NATIVE); + expect(placement.native).to.equal(native); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/medianetBidAdapter_spec.js b/test/spec/modules/medianetBidAdapter_spec.js index 1b2207de842..1eeb167601e 100644 --- a/test/spec/modules/medianetBidAdapter_spec.js +++ b/test/spec/modules/medianetBidAdapter_spec.js @@ -952,6 +952,42 @@ let VALID_BID_REQUEST = [{ } } }, + SERVER_VIDEO_OUTSTREAM_RESPONSE_VALID_BID = { + body: { + 'id': 'd90ca32f-3877-424a-b2f2-6a68988df57a', + 'bidList': [{ + 'no_bid': false, + 'requestId': '27210feac00e96', + 'cpm': 12.00, + 'width': 640, + 'height': 480, + 'ttl': 180, + 'creativeId': '370637746', + 'netRevenue': true, + 'vastXml': '', + 'currency': 'USD', + 'dfp_id': 'video1', + 'mediaType': 'video', + 'vto': 5000, + 'mavtr': 10, + 'avp': true, + 'ap': true, + 'pl': true, + 'mt': true, + 'jslt': 3000, + 'context': 'outstream' + }], + 'ext': { + 'csUrl': [{ + 'type': 'image', + 'url': 'http://cs.media.net/cksync.php' + }, { + 'type': 'iframe', + 'url': 'http://contextual.media.net/checksync.php?&vsSync=1' + }] + } + } + }, SERVER_VALID_BIDS = [{ 'no_bid': false, 'requestId': '27210feac00e96', @@ -1405,4 +1441,9 @@ describe('Media.net bid adapter', function () { expect(response).to.deep.equal(undefined); }); }); + + it('context should be outstream', function () { + let bids = spec.interpretResponse(SERVER_VIDEO_OUTSTREAM_RESPONSE_VALID_BID, []); + expect(bids[0].context).to.equal('outstream'); + }); }); diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index 5d930f2b6ac..3c18cfe0be7 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -24,6 +24,43 @@ describe('MediaSquare bid adapter tests', function () { code: 'publishername_atf_desktop_rg_pave' }, }]; + var VIDEO_PARAMS = [{ + adUnitCode: 'banner-div', + bidId: 'aaaa1234', + auctionId: 'bbbb1234', + transactionId: 'cccc1234', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + } + }, + bidder: 'mediasquare', + params: { + owner: 'test', + code: 'publishername_atf_desktop_rg_pave' + }, + }]; + var NATIVE_PARAMS = [{ + adUnitCode: 'banner-div', + bidId: 'aaaa1234', + auctionId: 'bbbb1234', + transactionId: 'cccc1234', + mediaTypes: { + native: { + title: { + required: true, + len: 80 + }, + } + }, + bidder: 'mediasquare', + params: { + owner: 'test', + code: 'publishername_atf_desktop_rg_pave' + }, + }]; var BID_RESPONSE = {'body': { 'responses': [{ @@ -128,4 +165,23 @@ describe('MediaSquare bid adapter tests', function () { expect(syncs[0]).to.have.property('type').and.to.equal('image'); expect(syncs[0]).to.have.property('url').and.to.equal('http://www.cookie.sync.org/'); }); + it('Verifies native in bid response', function () { + const request = spec.buildRequests(NATIVE_PARAMS, DEFAULT_OPTIONS); + BID_RESPONSE.body.responses[0].native = {'title': 'native title'}; + const response = spec.interpretResponse(BID_RESPONSE, request); + expect(response).to.have.lengthOf(1); + const bid = response[0]; + expect(bid).to.have.property('native'); + delete BID_RESPONSE.body.responses[0].native; + }); + it('Verifies video in bid response', function () { + const request = spec.buildRequests(VIDEO_PARAMS, DEFAULT_OPTIONS); + BID_RESPONSE.body.responses[0].video = {'xml': 'my vast XML', 'url': 'my vast url'}; + const response = spec.interpretResponse(BID_RESPONSE, request); + expect(response).to.have.lengthOf(1); + const bid = response[0]; + expect(bid).to.have.property('vastXml'); + expect(bid).to.have.property('vastUrl'); + delete BID_RESPONSE.body.responses[0].video; + }); }); diff --git a/test/spec/modules/mobfoxpbBidAdapter_spec.js b/test/spec/modules/mobfoxpbBidAdapter_spec.js new file mode 100644 index 00000000000..a02d580ab88 --- /dev/null +++ b/test/spec/modules/mobfoxpbBidAdapter_spec.js @@ -0,0 +1,304 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/mobfoxpbBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('MobfoxHBBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'mobfoxpb', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 783, + traffic: BANNER + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://bes.mobfox.com/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement.placementId).to.equal(783); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + }); + + it('Returns valid data for mediatype video', function () { + const playerSize = [300, 300]; + bid.mediaTypes = {}; + bid.params.traffic = VIDEO; + bid.mediaTypes[VIDEO] = { + playerSize + }; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain'); + expect(placement.traffic).to.equal(VIDEO); + expect(placement.wPlayer).to.equal(playerSize[0]); + expect(placement.hPlayer).to.equal(playerSize[1]); + }); + + it('Returns valid data for mediatype native', function () { + const native = { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + }; + + bid.mediaTypes = {}; + bid.params.traffic = NATIVE; + bid.mediaTypes[NATIVE] = native; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'native', 'schain'); + expect(placement.traffic).to.equal(NATIVE); + expect(placement.native).to.equal(native); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/nobidBidAdapter_spec.js b/test/spec/modules/nobidBidAdapter_spec.js index 346356e7d5b..e67d3b41f1d 100644 --- a/test/spec/modules/nobidBidAdapter_spec.js +++ b/test/spec/modules/nobidBidAdapter_spec.js @@ -228,6 +228,75 @@ describe('Nobid Adapter', function () { }); }); + describe('buildRequestsEIDs', function () { + const SITE_ID = 2; + const REFERER = 'https://www.examplereferer.com'; + let bidRequests = [ + { + 'bidder': 'nobid', + 'params': { + 'siteId': SITE_ID + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'userIdAsEids': [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'CRITEO_ID', + 'atype': 1 + } + ] + }, + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5_ID', + 'atype': 1 + } + ], + 'ext': { + 'linkType': 0 + } + }, + { + 'source': 'adserver.org', + 'uids': [ + { + 'id': 'TD_ID', + 'atype': 1, + 'ext': { + 'rtiPartner': 'TDID' + } + } + ] + } + ] + } + ]; + + let bidderRequest = { + refererInfo: {referer: REFERER} + } + + it('should criteo eid', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.sid).to.exist.and.to.equal(2); + expect(payload.eids[0].source).to.exist.and.to.equal('criteo.com'); + expect(payload.eids[0].uids[0].id).to.exist.and.to.equal('CRITEO_ID'); + expect(payload.eids[1].source).to.exist.and.to.equal('id5-sync.com'); + expect(payload.eids[1].uids[0].id).to.exist.and.to.equal('ID5_ID'); + expect(payload.eids[2].source).to.exist.and.to.equal('adserver.org'); + expect(payload.eids[2].uids[0].id).to.exist.and.to.equal('TD_ID'); + }); + }); + describe('buildRequests', function () { const SITE_ID = 2; const REFERER = 'https://www.examplereferer.com'; diff --git a/test/spec/modules/oneVideoBidAdapter_spec.js b/test/spec/modules/oneVideoBidAdapter_spec.js index ae29bcd48ec..331ac8976e6 100644 --- a/test/spec/modules/oneVideoBidAdapter_spec.js +++ b/test/spec/modules/oneVideoBidAdapter_spec.js @@ -315,6 +315,64 @@ describe('OneVideoBidAdapter', function () { const schain = data.source.ext.schain; expect(schain.nodes[0].hp).to.equal(bidRequest.params.video.hp); }) + it('should not accept key values pairs if custom is Undefined ', function () { + bidRequest.params.video.custom = null; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.undefined; + }); + it('should not accept key values pairs if custom is Array ', function () { + bidRequest.params.video.custom = []; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.undefined; + }); + it('should not accept key values pairs if custom is Number ', function () { + bidRequest.params.video.custom = 123456; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.undefined; + }); + it('should not accept key values pairs if custom is String ', function () { + bidRequest.params.video.custom = 'keyValuePairs'; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.undefined; + }); + it('should not accept key values pairs if custom is Boolean ', function () { + bidRequest.params.video.custom = true; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.undefined; + }); + it('should accept key values pairs if custom is Object ', function () { + bidRequest.params.video.custom = {}; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.a('object'); + }); + it('should accept key values pairs if custom is Object ', function () { + bidRequest.params.video.custom = { + key1: 'value1', + key2: 'value2', + key3: 4444444, + key4: false, + key5: {nested: 'object'}, + key6: ['string', 2, true, null], + key7: null, + key8: undefined + }; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const custom = requests[0].data.imp[0].ext.custom; + expect(custom['key1']).to.be.a('string'); + expect(custom['key2']).to.be.a('string'); + expect(custom['key3']).to.be.a('number'); + expect(custom['key4']).to.not.exist; + expect(custom['key5']).to.not.exist; + expect(custom['key6']).to.not.exist; + expect(custom['key7']).to.not.exist; + expect(custom['key8']).to.not.exist; + }); }); describe('spec.interpretResponse', function () { diff --git a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js index ce97789fe3e..ef7cb2bbe3b 100644 --- a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js +++ b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js @@ -1,6 +1,8 @@ import prebidmanagerAnalytics from 'modules/prebidmanagerAnalyticsAdapter.js'; import {expect} from 'chai'; import {server} from 'test/mocks/xhr.js'; +import * as utils from 'src/utils.js'; + let events = require('src/events'); let constants = require('src/constants.json'); @@ -98,7 +100,7 @@ describe('Prebid Manager Analytics Adapter', function () { events.emit(constants.EVENTS.AUCTION_END, {}); events.emit(constants.EVENTS.BID_TIMEOUT, {}); - sinon.assert.callCount(prebidmanagerAnalytics.track, 7); + sinon.assert.callCount(prebidmanagerAnalytics.track, 6); }); }); @@ -135,4 +137,23 @@ describe('Prebid Manager Analytics Adapter', function () { expect(pmEvents.utmTags.utm_content).to.equal(''); }); }); + + describe('build page info', function () { + afterEach(function () { + prebidmanagerAnalytics.disableAnalytics() + }); + it('should build page info', function () { + prebidmanagerAnalytics.enableAnalytics({ + provider: 'prebidmanager', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.pageInfo.domain).to.equal(window.location.hostname); + expect(pmEvents.pageInfo.referrerDomain).to.equal(utils.parseUrl(document.referrer).hostname); + }); + }); }); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index ae45244f03d..8c673d29701 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -34,6 +34,34 @@ describe('the price floors module', function () { '*': 2.5 } }; + const basicFloorDataHigh = { + floorMin: 7.0, + modelVersion: 'basic model', + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + 'video': 5.0, + '*': 2.5 + } + }; + const basicFloorDataLow = { + floorMin: 2.3, + modelVersion: 'basic model', + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + 'video': 5.0, + '*': 2.5 + } + }; const basicFloorConfig = { enabled: true, auctionDelay: 0, @@ -46,6 +74,32 @@ describe('the price floors module', function () { }, data: basicFloorData } + const minFloorConfigHigh = { + enabled: true, + auctionDelay: 0, + floorMin: 7, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: basicFloorDataHigh + } + const minFloorConfigLow = { + enabled: true, + auctionDelay: 0, + floorMin: 2.3, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: basicFloorDataLow + } const basicBidRequest = { bidder: 'rubicon', adUnitCode: 'test_div_1', @@ -165,22 +219,50 @@ describe('the price floors module', function () { it('selects the right floor for different mediaTypes', function () { // banner with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.0, matchingFloor: 1.0, matchingData: 'banner', matchingRule: 'banner' }); // video with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 5.0, matchingFloor: 5.0, matchingData: 'video', matchingRule: 'video' }); // native (not in the rule list) with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'native', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 2.5, matchingFloor: 2.5, matchingData: 'native', matchingRule: '*' }); + // banner with floorMin higher than matching rule + handleSetFloorsConfig({ + ...minFloorConfigHigh + }); + expect(getFirstMatchingFloor({...basicFloorDataHigh}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 7, + floorRuleValue: 1.0, + matchingFloor: 7, + matchingData: 'banner', + matchingRule: 'banner' + }); + // banner with floorMin higher than matching rule + handleSetFloorsConfig({ + ...minFloorConfigLow + }); + expect(getFirstMatchingFloor({...basicFloorDataLow}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ + floorMin: 2.3, + floorRuleValue: 5, + matchingFloor: 5, + matchingData: 'video', + matchingRule: 'video' + }); }); it('does not alter cached matched input if conversion occurs', function () { let inputData = {...basicFloorData}; @@ -188,6 +270,8 @@ describe('the price floors module', function () { let result = getFirstMatchingFloor(inputData, basicBidRequest, {mediaType: 'banner', size: '*'}); // result should always be the same expect(result).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.0, matchingFloor: 1.0, matchingData: 'banner', matchingRule: 'banner' @@ -213,24 +297,32 @@ describe('the price floors module', function () { } // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: '300x250', matchingRule: '300x250' }); // video with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: '300x250', matchingRule: '300x250' }); // native (not in the rule list) with 300x600 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'native', size: [600, 300]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 4.4, matchingFloor: 4.4, matchingData: '600x300', matchingRule: '600x300' }); // n/a mediaType with a size not in file should go to catch all expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: undefined, size: [1, 1]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 5.5, matchingFloor: 5.5, matchingData: '1x1', matchingRule: '*' @@ -254,12 +346,16 @@ describe('the price floors module', function () { }; // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: 'test_div_1^banner^300x250', matchingRule: 'test_div_1^banner^300x250' }); // video with 300x250 size -> No matching rule so should use default expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0.5, matchingFloor: 0.5, matchingData: 'test_div_1^video^300x250', matchingRule: undefined @@ -267,6 +363,8 @@ describe('the price floors module', function () { // remove default and should still return the same floor as above since matches are cached delete inputFloorData.default; expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0.5, matchingFloor: 0.5, matchingData: 'test_div_1^video^300x250', matchingRule: undefined @@ -274,6 +372,8 @@ describe('the price floors module', function () { // update adUnitCode to test_div_2 with weird other params let newBidRequest = { ...basicBidRequest, adUnitCode: 'test_div_2' } expect(getFirstMatchingFloor(inputFloorData, newBidRequest, {mediaType: 'badmediatype', size: [900, 900]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 3.3, matchingFloor: 3.3, matchingData: 'test_div_2^badmediatype^900x900', matchingRule: 'test_div_2^*^*' @@ -327,6 +427,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(false, { skipped: true, + floorMin: undefined, modelVersion: undefined, location: 'noData', skipRate: 0, @@ -358,11 +459,46 @@ describe('the price floors module', function () { } }; runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]); + validateBidRequests(true, { + skipped: false, + floorMin: undefined, + modelVersion: 'adUnit Model Version', + location: 'adUnit', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined + }); + }); + it('should use adUnit level data and minFloor should be set', function () { + handleSetFloorsConfig({ + ...minFloorConfigHigh, + data: undefined + }); + // attach floor data onto an adUnit and run an auction + let adUnitWithFloors1 = { + ...getAdUnitMock('adUnit-Div-1'), + floors: { + ...basicFloorData, + modelVersion: 'adUnit Model Version', // change the model name + } + }; + let adUnitWithFloors2 = { + ...getAdUnitMock('adUnit-Div-2'), + floors: { + ...basicFloorData, + values: { + 'banner': 5.0, + '*': 10.4 + } + } + }; + runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]); validateBidRequests(true, { skipped: false, modelVersion: 'adUnit Model Version', location: 'adUnit', skipRate: 0, + floorMin: 7, fetchStatus: undefined, floorProvider: undefined }); @@ -372,6 +508,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -392,6 +529,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -405,6 +543,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -418,6 +557,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -440,6 +580,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 50, @@ -453,6 +594,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 10, @@ -466,6 +608,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -529,6 +672,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-1', location: 'setConfig', skipRate: 0, @@ -541,6 +685,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-2', location: 'setConfig', skipRate: 0, @@ -553,6 +698,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-3', location: 'setConfig', skipRate: 0, @@ -581,6 +727,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -659,6 +806,7 @@ describe('the price floors module', function () { // the exposedAdUnits should be from the fetch not setConfig level data validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -696,6 +844,7 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', location: 'fetch', skipRate: 0, @@ -732,6 +881,7 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', location: 'fetch', skipRate: 0, @@ -771,6 +921,7 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', location: 'fetch', skipRate: 95, @@ -792,6 +943,7 @@ describe('the price floors module', function () { // and fetch failed is true validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -815,6 +967,7 @@ describe('the price floors module', function () { // and fetchStatus is 'success' but location is setConfig since it had bad data validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -1303,6 +1456,7 @@ describe('the price floors module', function () { runBidResponse(); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ + floorRuleValue: 0.3, floorValue: 0.3, floorCurrency: 'USD', floorRule: 'banner', @@ -1340,6 +1494,7 @@ describe('the price floors module', function () { expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ floorValue: 0.5, + floorRuleValue: 0.5, floorCurrency: 'USD', floorRule: 'banner|300x250', cpmAfterAdjustments: 0.5, @@ -1366,6 +1521,7 @@ describe('the price floors module', function () { }); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ + floorRuleValue: 5.5, floorValue: 5.5, floorCurrency: 'USD', floorRule: 'video|*', diff --git a/test/spec/modules/pubgeniusBidAdapter_spec.js b/test/spec/modules/pubgeniusBidAdapter_spec.js index 52f2e3aeefe..382199dcffc 100644 --- a/test/spec/modules/pubgeniusBidAdapter_spec.js +++ b/test/spec/modules/pubgeniusBidAdapter_spec.js @@ -1,8 +1,9 @@ import { expect } from 'chai'; import { spec } from 'modules/pubgeniusBidAdapter.js'; -import { deepClone, parseQueryStringParameters } from 'src/utils.js'; import { config } from 'src/config.js'; +import { VIDEO } from 'src/mediaTypes.js'; +import { deepClone, parseQueryStringParameters } from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; const { @@ -23,8 +24,8 @@ describe('pubGENIUS adapter', () => { }); describe('supportedMediaTypes', () => { - it('should contain only banner', () => { - expect(supportedMediaTypes).to.deep.equal(['banner']); + it('should contain banner and video', () => { + expect(supportedMediaTypes).to.deep.equal(['banner', 'video']); }); }); @@ -77,6 +78,51 @@ describe('pubGENIUS adapter', () => { expect(isBidRequestValid(bid)).to.be.false; }); + + it('should return false without banner or video', () => { + bid.mediaTypes = {}; + + expect(isBidRequestValid(bid)).to.be.false; + }); + + it('should return true with valid video media type', () => { + bid.mediaTypes = { + video: { + context: 'instream', + playerSize: [[100, 100]], + mimes: ['video/mp4'], + protocols: [1], + }, + }; + + expect(isBidRequestValid(bid)).to.be.true; + }); + + it('should return true with valid video params', () => { + bid.params.video = { + placement: 1, + w: 200, + h: 200, + mimes: ['video/mp4'], + protocols: [1], + }; + + expect(isBidRequestValid(bid)).to.be.true; + }); + + it('should return false without video protocols', () => { + bid.mediaTypes = { + video: { + context: 'instream', + playerSize: [[100, 100]], + }, + }; + bid.params.video = { + mimes: ['video/mp4'], + }; + + expect(isBidRequestValid(bid)).to.be.false; + }); }); describe('buildRequests', () => { @@ -122,6 +168,7 @@ describe('pubGENIUS adapter', () => { bidderCode: 'pubgenius', bidderRequestId: 'fakebidderrequestid', refererInfo: {}, + timeout: 1200, }; expectedRequest = { @@ -142,14 +189,14 @@ describe('pubGENIUS adapter', () => { tmax: 1200, ext: { pbadapter: { - version: '1.0.0', + version: '1.1.0', }, }, }, }; config.setConfig({ - bidderTimeout: 1200, + bidderTimeout: 1000, pageUrl: undefined, coppa: undefined, }); @@ -313,6 +360,44 @@ describe('pubGENIUS adapter', () => { expect(buildRequests([bidRequest], bidderRequest)).to.deep.equal(expectedRequest); }); + + it('should build video imp', () => { + bidRequest.mediaTypes = { + video: { + context: 'instream', + playerSize: [[200, 100]], + mimes: ['video/mp4'], + protocols: [2, 3], + api: [1, 2], + playbackmethod: [3, 4], + }, + }; + bidRequest.params.video = { + minduration: 5, + maxduration: 100, + skip: 1, + skipafter: 1, + startdelay: -1, + }; + + delete expectedRequest.data.imp[0].banner; + expectedRequest.data.imp[0].video = { + mimes: ['video/mp4'], + minduration: 5, + maxduration: 100, + protocols: [2, 3], + w: 200, + h: 100, + startdelay: -1, + placement: 1, + skip: 1, + skipafter: 1, + playbackmethod: [3, 4], + api: [1, 2], + }; + + expect(buildRequests([bidRequest], bidderRequest)).to.deep.equal(expectedRequest); + }); }); describe('interpretResponse', () => { @@ -369,6 +454,29 @@ describe('pubGENIUS adapter', () => { it('should interpret no bids', () => { expect(interpretResponse({ body: {} })).to.deep.equal([]); }); + + it('should interpret video response', () => { + serverResponse.body.seatbid[0].bid[0] = { + ...serverResponse.body.seatbid[0].bid[0], + nurl: 'http://vasturl/cache?id=x', + ext: { + pbadapter: { + mediaType: 'video', + cacheKey: 'x', + }, + }, + }; + + delete expectedBidResponse.ad; + expectedBidResponse = { + ...expectedBidResponse, + vastUrl: 'http://vasturl/cache?id=x', + vastXml: 'fake_creative', + mediaType: VIDEO, + }; + + expect(interpretResponse(serverResponse)).to.deep.equal([expectedBidResponse]); + }); }); describe('getUserSyncs', () => { diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..91c81dcae8d --- /dev/null +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -0,0 +1,734 @@ +import pubxaiAnalyticsAdapter from 'modules/pubxaiAnalyticsAdapter.js'; +import { getDeviceType } from 'modules/pubxaiAnalyticsAdapter.js'; +import { + expect +} from 'chai'; +import adapterManager from 'src/adapterManager.js'; +import * as utils from 'src/utils.js'; +import { + server +} from 'test/mocks/xhr.js'; + +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('pubxai analytics adapter', function() { + beforeEach(function() { + sinon.stub(events, 'getEvents').returns([]); + }); + + afterEach(function() { + events.getEvents.restore(); + }); + + describe('track', function() { + let initOptions = { + samplingRate: '1', + pubxId: '6c415fc0-8b0e-4cf5-be73-01526a4db625' + }; + + let prebidEvent = { + 'auctionInit': { + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'timestamp': 1603865707180, + 'auctionStatus': 'inProgress', + 'adUnits': [{ + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + } + }], + 'sizes': [ + [ + 300, + 250 + ] + ], + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' + }], + 'adUnitCodes': [ + '/19968336/header-bid-tag-1' + ], + 'bidderRequests': [{ + 'bidderCode': 'appnexus', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'bidderRequestId': '184cbc05bb90ba', + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '248f9a4489835e', + 'bidderRequestId': '184cbc05bb90ba', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }], + 'auctionStart': 1603865707180, + 'timeout': 1000, + 'refererInfo': { + 'referer': 'http://local-pnh.net:8080/stream/', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://local-pnh.net:8080/stream/' + ], + 'canonicalUrl': null + }, + 'start': 1603865707182 + }], + 'noBids': [], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 1000, + 'config': { + 'samplingRate': '1', + 'pubxId': '6c415fc0-8b0e-4cf5-be73-01526a4db625' + } + }, + 'bidRequested': { + 'bidderCode': 'appnexus', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'bidderRequestId': '184cbc05bb90ba', + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '248f9a4489835e', + 'bidderRequestId': '184cbc05bb90ba', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }], + 'auctionStart': 1603865707180, + 'timeout': 1000, + 'refererInfo': { + 'referer': 'http://local-pnh.net:8080/stream/', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://local-pnh.net:8080/stream/' + ], + 'canonicalUrl': null + }, + 'start': 1603865707182 + }, + 'bidTimeout': [], + 'bidResponse': { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '32780c4bc382cb', + 'requestId': '248f9a4489835e', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'meta': { + 'advertiserId': 2529885 + }, + 'ad': '', + 'originalCpm': 0.5, + 'originalCurrency': 'USD', + 'floorData': { + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'responseTimestamp': 1603865707449, + 'requestTimestamp': 1603865707182, + 'bidder': 'appnexus', + 'timeToRespond': 267, + 'pbLg': '0.50', + 'pbMg': '0.50', + 'pbHg': '0.50', + 'pbAg': '0.50', + 'pbDg': '0.50', + 'pbCg': '0.50', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '32780c4bc382cb', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + }, + 'auctionEnd': { + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'timestamp': 1603865707180, + 'auctionEnd': 1603865707180, + 'auctionStatus': 'completed', + 'adUnits': [{ + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + } + }], + 'sizes': [ + [ + 300, + 250 + ] + ], + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' + }], + 'adUnitCodes': [ + '/19968336/header-bid-tag-1' + ], + 'bidderRequests': [{ + 'bidderCode': 'appnexus', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'bidderRequestId': '184cbc05bb90ba', + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '248f9a4489835e', + 'bidderRequestId': '184cbc05bb90ba', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }], + 'auctionStart': 1603865707180, + 'timeout': 1000, + 'refererInfo': { + 'referer': 'http://local-pnh.net:8080/stream/', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://local-pnh.net:8080/stream/' + ], + 'canonicalUrl': null + }, + 'start': 1603865707182 + }], + 'noBids': [], + 'bidsReceived': [{ + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '32780c4bc382cb', + 'requestId': '248f9a4489835e', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'meta': { + 'advertiserId': 2529885 + }, + 'ad': '', + 'originalCpm': 0.5, + 'originalCurrency': 'USD', + 'floorData': { + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'responseTimestamp': 1603865707449, + 'requestTimestamp': 1603865707182, + 'bidder': 'appnexus', + 'timeToRespond': 267, + 'pbLg': '0.50', + 'pbMg': '0.50', + 'pbHg': '0.50', + 'pbAg': '0.50', + 'pbDg': '0.50', + 'pbCg': '0.50', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '32780c4bc382cb', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + 'status': 'rendered', + 'params': [{ + 'placementId': 13144370 + }] + }], + 'winningBids': [], + 'timeout': 1000 + }, + 'bidWon': { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '32780c4bc382cb', + 'requestId': '248f9a4489835e', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'meta': { + 'advertiserId': 2529885 + }, + 'ad': '', + 'originalCpm': 0.5, + 'originalCurrency': 'USD', + 'floorData': { + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'responseTimestamp': 1603865707449, + 'requestTimestamp': 1603865707182, + 'bidder': 'appnexus', + 'timeToRespond': 267, + 'pbLg': '0.50', + 'pbMg': '0.50', + 'pbHg': '0.50', + 'pbAg': '0.50', + 'pbDg': '0.50', + 'pbCg': '0.50', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '32780c4bc382cb', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + 'status': 'rendered', + 'params': [{ + 'placementId': 13144370 + }] + } + }; + let location = utils.getWindowLocation(); + + let expectedAfterBid = { + 'bids': [{ + 'bidderCode': 'appnexus', + 'bidId': '248f9a4489835e', + 'adUnitCode': '/19968336/header-bid-tag-1', + 'requestId': '184cbc05bb90ba', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'sizes': '300x250', + 'renderStatus': 2, + 'requestTimestamp': 1603865707182, + 'creativeId': 96846035, + 'currency': 'USD', + 'cpm': 0.5, + 'netRevenue': true, + 'mediaType': 'banner', + 'statusMessage': 'Bid available', + 'floorData': { + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'timeToRespond': 267, + 'responseTimestamp': 1603865707449, + 'platform': navigator.platform, + 'deviceType': getDeviceType() + }], + 'auctionInit': { + 'host': location.host, + 'path': location.pathname, + 'search': location.search, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'timestamp': 1603865707180, + 'auctionStatus': 'inProgress', + 'adUnits': [{ + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + } + }], + 'sizes': [ + [ + 300, + 250 + ] + ], + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' + }], + 'adUnitCodes': [ + '/19968336/header-bid-tag-1' + ], + 'bidderRequests': [{ + 'bidderCode': 'appnexus', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'bidderRequestId': '184cbc05bb90ba', + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '248f9a4489835e', + 'bidderRequestId': '184cbc05bb90ba', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }], + 'auctionStart': 1603865707180, + 'timeout': 1000, + 'refererInfo': { + 'referer': 'http://local-pnh.net:8080/stream/', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://local-pnh.net:8080/stream/' + ], + 'canonicalUrl': null + }, + 'start': 1603865707182 + }], + 'noBids': [], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 1000, + 'config': { + 'samplingRate': '1', + 'pubxId': '6c415fc0-8b0e-4cf5-be73-01526a4db625' + } + }, + 'initOptions': initOptions + }; + + let expectedAfterBidWon = { + 'winningBid': { + 'bidderCode': 'appnexus', + 'bidId': '248f9a4489835e', + 'adUnitCode': '/19968336/header-bid-tag-1', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'renderedSize': '300x250', + 'renderStatus': 4, + 'requestTimestamp': 1603865707182, + 'creativeId': 96846035, + 'currency': 'USD', + 'cpm': 0.5, + 'netRevenue': true, + 'mediaType': 'banner', + 'status': 'rendered', + 'statusMessage': 'Bid available', + 'floorData': { + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'timeToRespond': 267, + 'responseTimestamp': 1603865707449, + 'platform': navigator.platform, + 'deviceType': getDeviceType() + }, + 'initOptions': initOptions + } + + adapterManager.registerAnalyticsAdapter({ + code: 'pubxai', + adapter: pubxaiAnalyticsAdapter + }); + + beforeEach(function() { + adapterManager.enableAnalytics({ + provider: 'pubxai', + options: initOptions + }); + }); + + afterEach(function() { + pubxaiAnalyticsAdapter.disableAnalytics(); + }); + + it('builds and sends auction data', function() { + // Step 1: Send auction init event + events.emit(constants.EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + // Step 2: Send bid requested event + events.emit(constants.EVENTS.BID_REQUESTED, prebidEvent['bidRequested']); + + // Step 3: Send bid response event + events.emit(constants.EVENTS.BID_RESPONSE, prebidEvent['bidResponse']); + + // Step 4: Send bid time out event + events.emit(constants.EVENTS.BID_TIMEOUT, prebidEvent['bidTimeout']); + + // Step 5: Send auction end event + events.emit(constants.EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + + expect(server.requests.length).to.equal(1); + + let realAfterBid = JSON.parse(server.requests[0].requestBody); + + expect(realAfterBid).to.deep.equal(expectedAfterBid); + + // Step 6: Send auction bid won event + events.emit(constants.EVENTS.BID_WON, prebidEvent['bidWon']); + + expect(server.requests.length).to.equal(2); + + let winEventData = JSON.parse(server.requests[1].requestBody); + + expect(winEventData).to.deep.equal(expectedAfterBidWon); + }); + }); +}); diff --git a/test/spec/modules/pulsepointBidAdapter_spec.js b/test/spec/modules/pulsepointBidAdapter_spec.js index a6f5ff2a0dc..c3830d5cb46 100644 --- a/test/spec/modules/pulsepointBidAdapter_spec.js +++ b/test/spec/modules/pulsepointBidAdapter_spec.js @@ -19,6 +19,11 @@ describe('PulsePoint Adapter Tests', function () { } }, { placementCode: '/DfpAccount2/slot2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, bidId: 'bid23456', params: { cp: 'p10000', @@ -72,6 +77,11 @@ describe('PulsePoint Adapter Tests', function () { }]; const additionalParamsConfig = [{ placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, bidId: 'bid12345', params: { cp: 'p10000', @@ -89,6 +99,11 @@ describe('PulsePoint Adapter Tests', function () { const ortbParamsSlotConfig = [{ placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, bidId: 'bid12345', params: { cp: 'p10000', @@ -146,6 +161,11 @@ describe('PulsePoint Adapter Tests', function () { const schainParamsSlotConfig = [{ placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, bidId: 'bid12345', params: { cp: 'p10000', @@ -681,7 +701,10 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.imp[1].banner).to.not.be.null; expect(ortbRequest.imp[1].banner.w).to.equal(728); expect(ortbRequest.imp[1].banner.h).to.equal(90); - expect(ortbRequest.imp[1].banner.format).to.be.null; + expect(ortbRequest.imp[1].banner.format).to.not.be.null; + expect(ortbRequest.imp[1].banner.format).to.have.lengthOf(1); + expect(ortbRequest.imp[1].banner.format[0].w).to.equal(728); + expect(ortbRequest.imp[1].banner.format[0].h).to.equal(90); // adsize on response const ortbResponse = { seatbid: [{ @@ -701,4 +724,58 @@ describe('PulsePoint Adapter Tests', function () { expect(bid.width).to.equal(728); expect(bid.height).to.equal(90); }); + it('Verify multi-format response', function () { + const bidRequests = deepClone(slotConfigs); + bidRequests[0].mediaTypes['native'] = { + title: { + required: true + }, + image: { + required: true + }, + sponsoredBy: { + required: true + } + }; + bidRequests[1].params.video = { + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request).to.be.not.null; + expect(request.data).to.be.not.null; + const ortbRequest = request.data; + expect(ortbRequest.imp).to.have.lengthOf(2); + // adsize on response + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + crid: 'Creative#123', + w: 728, + h: 90 + }, { + impid: ortbRequest.imp[1].id, + price: 2.5, + adm: '', + crid: 'Creative#234', + w: 728, + h: 90 + }] + }] + }; + // request has both types - banner and native, response is parsed as banner. + // for impression#2, response is parsed as video + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(2); + const bid = bids[0]; + expect(bid.width).to.equal(728); + expect(bid.height).to.equal(90); + const secondBid = bids[1]; + expect(secondBid.vastXml).to.equal(''); + }); }); diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index caa554c8cd8..5b4e7963e60 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -7,7 +7,8 @@ import { QUANTCAST_TEST_PUBLISHER, QUANTCAST_PROTOCOL, QUANTCAST_PORT, - spec as qcSpec + spec as qcSpec, + storage } from '../../../modules/quantcastBidAdapter.js'; import { newBidder } from '../../../src/adapters/bidderFactory.js'; import { parseUrl } from 'src/utils.js'; @@ -42,6 +43,8 @@ describe('Quantcast adapter', function () { canonicalUrl: 'http://example.com/hello.html' } }; + + storage.setCookie('__qca', '', 'Thu, 01 Jan 1970 00:00:00 GMT'); }); function setupVideoBidRequest(videoParams) { @@ -140,7 +143,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; it('sends banner bid requests contains all the required parameters', function () { @@ -208,7 +212,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedVideoBidRequest)); @@ -244,7 +249,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedVideoBidRequest)); @@ -276,7 +282,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedVideoBidRequest)); @@ -340,7 +347,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedBidRequest)); @@ -584,6 +592,13 @@ describe('Quantcast adapter', function () { expect(parsed.uspConsent).to.equal('consentString'); }); + it('propagates Quantcast first-party cookie (fpa)', function() { + storage.setCookie('__qca', 'P0-TestFPA'); + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + const parsed = JSON.parse(requests[0].data); + expect(parsed.fpa).to.equal('P0-TestFPA'); + }); + describe('propagates coppa', function() { let sandbox; beforeEach(() => { diff --git a/test/spec/modules/qwarryBidAdapter_spec.js b/test/spec/modules/qwarryBidAdapter_spec.js index a5bb438f384..91e3cf4bfdf 100644 --- a/test/spec/modules/qwarryBidAdapter_spec.js +++ b/test/spec/modules/qwarryBidAdapter_spec.js @@ -6,37 +6,42 @@ const REQUEST = { 'bidId': '456', 'bidder': 'qwarry', 'params': { - zoneToken: 'e64782a4-8e68-4c38-965b-80ccf115d46f' + zoneToken: 'e64782a4-8e68-4c38-965b-80ccf115d46f', + pos: 7 } } -const BIDDER_BANNER_RESPONSE = {'prebidResponse': [{ - 'ad': '
test
', - 'requestId': 'e64782a4-8e68-4c38-965b-80ccf115d46d', - 'cpm': 900.5, - 'currency': 'USD', - 'width': 640, - 'height': 480, - 'ttl': 300, - 'creativeId': 1, - 'netRevenue': true, - 'winUrl': 'http://test.com', - 'format': 'banner' -}]} - -const BIDDER_VIDEO_RESPONSE = {'prebidResponse': [{ - 'ad': 'vast', - 'requestId': 'e64782a4-8e68-4c38-965b-80ccf115d46z', - 'cpm': 800.4, - 'currency': 'USD', - 'width': 1024, - 'height': 768, - 'ttl': 200, - 'creativeId': 2, - 'netRevenue': true, - 'winUrl': 'http://test.com', - 'format': 'video' -}]} +const BIDDER_BANNER_RESPONSE = { + 'prebidResponse': [{ + 'ad': '
test
', + 'requestId': 'e64782a4-8e68-4c38-965b-80ccf115d46d', + 'cpm': 900.5, + 'currency': 'USD', + 'width': 640, + 'height': 480, + 'ttl': 300, + 'creativeId': 1, + 'netRevenue': true, + 'winUrl': 'http://test.com', + 'format': 'banner' + }] +} + +const BIDDER_VIDEO_RESPONSE = { + 'prebidResponse': [{ + 'ad': 'vast', + 'requestId': 'e64782a4-8e68-4c38-965b-80ccf115d46z', + 'cpm': 800.4, + 'currency': 'USD', + 'width': 1024, + 'height': 768, + 'ttl': 200, + 'creativeId': 2, + 'netRevenue': true, + 'winUrl': 'http://test.com', + 'format': 'video' + }] +} const BIDDER_NO_BID_RESPONSE = '' @@ -70,7 +75,7 @@ describe('qwarryBidAdapter', function () { it('sends bid request to ENDPOINT via POST', function () { expect(bidderRequest.method).to.equal('POST') expect(bidderRequest.data.requestId).to.equal('123') - expect(bidderRequest.data.bids).to.deep.contains({ bidId: '456', zoneToken: 'e64782a4-8e68-4c38-965b-80ccf115d46f' }) + expect(bidderRequest.data.bids).to.deep.contains({ bidId: '456', zoneToken: 'e64782a4-8e68-4c38-965b-80ccf115d46f', pos: 7 }) expect(bidderRequest.options.customHeaders).to.deep.equal({ 'Rtb-Direct': true }) expect(bidderRequest.options.contentType).to.equal('application/json') expect(bidderRequest.url).to.equal(ENDPOINT) @@ -119,4 +124,13 @@ describe('qwarryBidAdapter', function () { expect(result).to.deep.equal([]) }) }) + + describe('onBidWon', function () { + it('handles banner win: should get true', function () { + const win = BIDDER_BANNER_RESPONSE.prebidResponse[0] + const bidWonResult = spec.onBidWon(win) + + expect(bidWonResult).to.equal(true) + }) + }) }) diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js new file mode 100644 index 00000000000..b84aef15feb --- /dev/null +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -0,0 +1,160 @@ +import * as rtdModule from 'modules/rtdModule/index.js'; +import { config } from 'src/config.js'; +import * as sinon from 'sinon'; + +const getBidRequestDataSpy = sinon.spy(); + +const validSM = { + name: 'validSM', + init: () => { return true }, + getTargetingData: (adUnitsCodes) => { + return {'ad2': {'key': 'validSM'}} + }, + getBidRequestData: getBidRequestDataSpy +}; + +const validSMWait = { + name: 'validSMWait', + init: () => { return true }, + getTargetingData: (adUnitsCodes) => { + return {'ad1': {'key': 'validSMWait'}} + }, + getBidRequestData: getBidRequestDataSpy +}; + +const invalidSM = { + name: 'invalidSM' +}; + +const failureSM = { + name: 'failureSM', + init: () => { return false } +}; + +const nonConfSM = { + name: 'nonConfSM', + init: () => { return true } +}; + +const conf = { + 'realTimeData': { + 'auctionDelay': 100, + dataProviders: [ + { + 'name': 'validSMWait', + 'waitForIt': true, + }, + { + 'name': 'validSM', + 'waitForIt': false, + }, + { + 'name': 'invalidSM' + }, + { + 'name': 'failureSM' + }] + } +}; + +describe('Real time module', function () { + before(function () { + rtdModule.attachRealTimeDataProvider(validSM); + rtdModule.attachRealTimeDataProvider(invalidSM); + rtdModule.attachRealTimeDataProvider(failureSM); + rtdModule.attachRealTimeDataProvider(nonConfSM); + rtdModule.attachRealTimeDataProvider(validSMWait); + }); + + after(function () { + config.resetConfig(); + }); + + beforeEach(function () { + config.setConfig(conf); + }); + + it('should use only valid modules', function () { + rtdModule.init(config); + expect(rtdModule.subModules).to.eql([validSMWait, validSM]); + }); + + it('should be able to modify bid request', function (done) { + rtdModule.setBidRequestsData(() => { + assert(getBidRequestDataSpy.calledTwice); + assert(getBidRequestDataSpy.calledWith({bidRequest: {}})); + done(); + }, {bidRequest: {}}) + }); + + 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('sould place targeting on adUnits', function (done) { + const auction = { + adUnitCodes: ['ad1', 'ad2'], + adUnits: [ + { + code: 'ad1' + }, + { + code: 'ad2', + adserverTargeting: {preKey: 'preValue'} + } + ] + }; + + const expectedAdUnits = [ + { + code: 'ad1', + adserverTargeting: {key: 'validSMWait'} + }, + { + code: 'ad2', + adserverTargeting: { + preKey: 'preValue', + key: 'validSM' + } + } + ]; + + const adUnits = rtdModule.getAdUnitTargeting(auction); + assert.deepEqual(expectedAdUnits, adUnits) + done(); + }) +}); diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js deleted file mode 100644 index f47068724d1..00000000000 --- a/test/spec/modules/realTimeModule_spec.js +++ /dev/null @@ -1,274 +0,0 @@ -import * as rtdModule from 'modules/rtdModule/index.js'; -import { config } from 'src/config.js'; -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, - dataProviders: [{ - 'name': 'browsi', - 'params': { - 'url': 'testUrl.com', - 'siteKey': 'testKey', - 'pubKey': 'testPub', - 'keyName': 'bv' - } - }] - } - }; - - beforeEach(function () { - config.setConfig(conf); - }); - - after(function () { - config.resetConfig(); - }); - - it('should init and return true', function () { - browsiRTD.beforeInit(config); - expect(browsiRTD.browsiSubmodule.init()).to.equal(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('should match placement with ad unit', function () { - const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - - 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 - - expect(test1).to.equal(true); - expect(test2).to.equal(true); - expect(test3).to.equal(false); - expect(test4).to.equal(true); - }); - - it('should return correct macro values', function () { - const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - - 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 macroResultB = browsiRTD.getMacroId({}, slot); - expect(macroResultB).to.equal('browsiAd_1'); - - const macroResultC = browsiRTD.getMacroId({p: '', s: {s: 0, e: 1}}, slot); - expect(macroResultC).to.equal('/'); - }); - - 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(); - } - }); - - 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('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(); - } - }) - }) -}); diff --git a/test/spec/modules/reconciliationRtdProvider_spec.js b/test/spec/modules/reconciliationRtdProvider_spec.js new file mode 100644 index 00000000000..6efe55ddf46 --- /dev/null +++ b/test/spec/modules/reconciliationRtdProvider_spec.js @@ -0,0 +1,221 @@ +import { + reconciliationSubmodule, + track, + getTopIFrameWin, + getSlotByWin +} from 'modules/reconciliationRtdProvider.js'; +import { makeSlot } from '../integration/faker/googletag.js'; +import * as utils from 'src/utils.js'; + +describe('Reconciliation Real time data submodule', function () { + const conf = { + dataProviders: [{ + 'name': 'reconciliation', + 'params': { + 'publisherMemberId': 'test_prebid_publisher' + }, + }] + }; + + let trackPostStub; + + beforeEach(function () { + trackPostStub = sinon.stub(track, 'trackPost'); + }); + + afterEach(function () { + trackPostStub.restore(); + }); + + describe('reconciliationSubmodule', function () { + describe('initialization', function () { + let utilsLogErrorSpy; + + before(function () { + utilsLogErrorSpy = sinon.spy(utils, 'logError'); + }); + + after(function () { + utils.logError.restore(); + }); + + it('successfully instantiates', function () { + expect(reconciliationSubmodule.init(conf.dataProviders[0])).to.equal(true); + }); + + it('should log error if initializied without parameters', function () { + expect(reconciliationSubmodule.init({'name': 'reconciliation', 'params': {}})).to.equal(true); + expect(utilsLogErrorSpy.calledOnce).to.be.true; + }); + }); + + describe('getData', function () { + it('should return data in proper format', function () { + makeSlot({code: '/reconciliationAdunit1', divId: 'reconciliationAd1'}); + + const targetingData = reconciliationSubmodule.getTargetingData(['/reconciliationAdunit1']); + expect(targetingData['/reconciliationAdunit1'].RSDK_AUID).to.eql('/reconciliationAdunit1'); + expect(targetingData['/reconciliationAdunit1'].RSDK_ADID).to.be.a('string'); + }); + + it('should return unit path if called with divId', function () { + makeSlot({code: '/reconciliationAdunit2', divId: 'reconciliationAd2'}); + + const targetingData = reconciliationSubmodule.getTargetingData(['reconciliationAd2']); + expect(targetingData['reconciliationAd2'].RSDK_AUID).to.eql('/reconciliationAdunit2'); + expect(targetingData['reconciliationAd2'].RSDK_ADID).to.be.a('string'); + }); + + it('should skip empty adUnit id', function () { + makeSlot({code: '/reconciliationAdunit3', divId: 'reconciliationAd3'}); + + const targetingData = reconciliationSubmodule.getTargetingData(['reconciliationAd3', '']); + expect(targetingData).to.have.all.keys('reconciliationAd3'); + }); + }); + + describe('track events', function () { + it('should track init event with data', function () { + const adUnit = { + code: '/adunit' + }; + + reconciliationSubmodule.getTargetingData([adUnit.code]); + + expect(trackPostStub.calledOnce).to.be.true; + expect(trackPostStub.getCalls()[0].args[0]).to.eql('https://confirm.fiduciadlt.com/init'); + expect(trackPostStub.getCalls()[0].args[1].adUnits[0].adUnitId).to.eql(adUnit.code); + expect(trackPostStub.getCalls()[0].args[1].adUnits[0].adDeliveryId).be.a('string'); + expect(trackPostStub.getCalls()[0].args[1].publisherMemberId).to.eql('test_prebid_publisher'); + }); + }); + + describe('get topmost iframe', function () { + /** + * - top + * -- iframe.window <-- top iframe window + * --- iframe.window + * ---- iframe.window <-- win + */ + const mockFrameWin = (topWin, parentWin) => { + return { + top: topWin, + parent: parentWin + } + } + + it('should return null if called with null', function() { + expect(getTopIFrameWin(null)).to.be.null; + }); + + it('should return null if there is an error in frames chain', function() { + const topWin = {}; + const iframe1Win = mockFrameWin(topWin, null); // break chain + const iframe2Win = mockFrameWin(topWin, iframe1Win); + + expect(getTopIFrameWin(iframe1Win, topWin)).to.be.null; + }); + + it('should get the topmost iframe', function () { + const topWin = {}; + const iframe1Win = mockFrameWin(topWin, topWin); + const iframe2Win = mockFrameWin(topWin, iframe1Win); + + expect(getTopIFrameWin(iframe2Win, topWin)).to.eql(iframe1Win); + }); + }); + + describe('get slot by nested iframe window', function () { + it('should return the slot', function () { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAd'; + adSlotElement.appendChild(adSlotIframe); + document.body.appendChild(adSlotElement); + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + + expect(getSlotByWin(adSlotIframe.contentWindow)).to.eql(adSlot); + }); + + it('should return null if the slot is not found', function () { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAd'; + document.body.appendChild(adSlotElement); + document.body.appendChild(adSlotIframe); // iframe is not in ad slot + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + + expect(getSlotByWin(adSlotIframe.contentWindow)).to.be.null; + }); + }); + + describe('handle postMessage from Reconciliation Tag in ad iframe', function () { + it('should track impression pixel with parameters', function (done) { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAdMessage'; + adSlotElement.appendChild(adSlotIframe); + document.body.appendChild(adSlotElement); + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + // Fix targeting methods + adSlot.targeting = {}; + adSlot.setTargeting = function(key, value) { + this.targeting[key] = [value]; + }; + adSlot.getTargeting = function(key) { + return this.targeting[key]; + }; + + adSlot.setTargeting('RSDK_AUID', '/reconciliationAdunit'); + adSlot.setTargeting('RSDK_ADID', '12345'); + adSlotIframe.contentDocument.open(); + adSlotIframe.contentDocument.write(``); + adSlotIframe.contentDocument.close(); + + setTimeout(() => { + expect(trackPostStub.calledOnce).to.be.true; + expect(trackPostStub.getCalls()[0].args[0]).to.eql('https://confirm.fiduciadlt.com/pimp'); + expect(trackPostStub.getCalls()[0].args[1].adUnitId).to.eql('/reconciliationAdunit'); + expect(trackPostStub.getCalls()[0].args[1].adDeliveryId).to.eql('12345'); + expect(trackPostStub.getCalls()[0].args[1].tagOwnerMemberId).to.eql('test_member_id'); ; + expect(trackPostStub.getCalls()[0].args[1].dataSources.length).to.eql(1); + expect(trackPostStub.getCalls()[0].args[1].dataRecipients.length).to.eql(2); + expect(trackPostStub.getCalls()[0].args[1].publisherMemberId).to.eql('test_prebid_publisher'); + done(); + }, 100); + }); + }); + }); +}); diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 42818232cda..ebc62752f16 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -1,16 +1,20 @@ import { expect } from 'chai'; import { spec } from 'modules/relaidoBidAdapter.js'; import * as utils from 'src/utils.js'; +import { getStorageManager } from '../../../src/storageManager.js'; const UUID_KEY = 'relaido_uuid'; const DEFAULT_USER_AGENT = window.navigator.userAgent; const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1'; +const relaido_uuid = 'hogehoge'; const setUADefault = () => { window.navigator.__defineGetter__('userAgent', function () { return DEFAULT_USER_AGENT }) }; const setUAMobile = () => { window.navigator.__defineGetter__('userAgent', function () { return MOBILE_USER_AGENT }) }; +const storage = getStorageManager(); +storage.setCookie(UUID_KEY, relaido_uuid); + describe('RelaidoAdapter', function () { - const relaido_uuid = 'hogehoge'; let bidRequest; let bidderRequest; let serverResponse; @@ -65,7 +69,6 @@ describe('RelaidoAdapter', function () { height: bidRequest.mediaTypes.video.playerSize[0][1], mediaType: 'video', }; - localStorage.setItem(UUID_KEY, relaido_uuid); }); describe('spec.isBidRequestValid', function () { @@ -140,12 +143,6 @@ describe('RelaidoAdapter', function () { setUADefault(); }); - it('should return false when the uuid are missing', function () { - localStorage.removeItem(UUID_KEY); - const result = !!(utils.isSafariBrowser()); - expect(spec.isBidRequestValid(bidRequest)).to.equal(result); - }); - it('should return false when the placementId params are missing', function () { bidRequest.params.placementId = undefined; expect(spec.isBidRequestValid(bidRequest)).to.equal(false); diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index 1c710c46ea2..90723fb863f 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -348,25 +348,30 @@ describe('Richaudience adapter tests', function () { }); describe('UID test', function () { - pbjs.setConfig({ + config.setConfig({ consentManagement: { cmpApi: 'iab', timeout: 5000, allowAuctionWithoutConsent: true + }, + userSync: { + userIds: [{ + name: 'id5Id', + params: { + partner: 173, // change to the Partner Number you received from ID5 + pd: 'MT1iNTBjY...' // optional, see table below for a link to how to generate this + }, + storage: { + type: 'html5', // "html5" is the required storage type + name: 'id5id', // "id5id" is the required storage name + expires: 90, // storage lasts for 90 days + refreshInSeconds: 8 * 3600 // refresh ID every 8 hours to ensure it's fresh + } + }], + auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules } }); it('Verify build id5', function () { - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: 'id5-user-id' }; - - var request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - var requestContent = JSON.parse(request[0].data); - - expect(requestContent.user).to.deep.equal([{ - 'userId': 'id5-user-id', - 'source': 'id5-sync.com' - }]); - var request; DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: 1 }; @@ -739,7 +744,7 @@ describe('Richaudience adapter tests', function () { }, [], {consentString: '', gdprApplies: true}); expect(syncs).to.have.lengthOf(0); - pbjs.setConfig({ + config.setConfig({ consentManagement: { cmpApi: 'iab', timeout: 5000, diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 2bbab506b34..4891b8d3282 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -111,10 +111,60 @@ const BID2 = Object.assign({}, BID, { } }); +const BID3 = Object.assign({}, BID, { + adUnitCode: '/19968336/siderail-tag1', + bidId: '5fg6hyy4r879f0', + adId: 'fake_ad_id', + requestId: '5fg6hyy4r879f0', + width: 300, + height: 250, + mediaType: 'banner', + cpm: 2.01, + source: 'server', + seatBidId: 'aaaa-bbbb-cccc-dddd', + rubiconTargeting: { + 'rpfl_elemid': '/19968336/siderail-tag1', + 'rpfl_14062': '15_tier0200' + }, + adserverTargeting: { + 'hb_bidder': 'rubicon', + 'hb_adid': '5fg6hyy4r879f0', + 'hb_pb': '2.00', + 'hb_size': '300x250', + 'hb_source': 'server' + } +}); + +const floorMinRequest = { + 'bidder': 'rubicon', + 'params': { + 'accountId': '14062', + 'siteId': '70608', + 'zoneId': '335918', + 'userId': '12346', + 'keywords': ['a', 'b', 'c'], + 'inventory': {'rating': '4-star', 'prodtype': 'tech'}, + 'visitor': {'ucat': 'new', 'lastsearch': 'iphone'}, + 'position': 'atf' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': '/19968336/siderail-tag1', + 'transactionId': 'c435626g-9e3f-401a-bee1-d56aec29a1d4', + 'sizes': [[300, 250]], + 'bidId': '5fg6hyy4r879f0', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' +}; + const MOCK = { SET_TARGETING: { [BID.adUnitCode]: BID.adserverTargeting, - [BID2.adUnitCode]: BID2.adserverTargeting + [BID2.adUnitCode]: BID2.adserverTargeting, + [BID3.adUnitCode]: BID3.adserverTargeting }, AUCTION_INIT: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', @@ -205,7 +255,8 @@ const MOCK = { 'sizes': [[640, 480]], 'bidId': '2ecff0db240757', 'bidderRequestId': '1be65d7958826a', - 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'src': 'client' }, { 'bidder': 'rubicon', @@ -229,7 +280,8 @@ const MOCK = { 'sizes': [[1000, 300], [970, 250], [728, 90]], 'bidId': '3bd4ebb1c900e2', 'bidderRequestId': '1be65d7958826a', - 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'src': 's2s' } ], 'auctionStart': 1519149536560, @@ -241,7 +293,8 @@ const MOCK = { }, BID_RESPONSE: [ BID, - BID2 + BID2, + BID3 ], AUCTION_END: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' @@ -252,14 +305,21 @@ const MOCK = { }), Object.assign({}, BID2, { 'status': 'rendered' + }), + Object.assign({}, BID3, { + 'status': 'rendered' }) ], BIDDER_DONE: { 'bidderCode': 'rubicon', + 'serverResponseTimeMs': 42, 'bids': [ BID, Object.assign({}, BID2, { 'serverResponseTimeMs': 42, + }), + Object.assign({}, BID3, { + 'serverResponseTimeMs': 55, }) ] }, @@ -842,14 +902,40 @@ describe('rubicon analytics adapter', function () { } }; + let floorMinResponse = { + ...BID3, + floorData: { + floorValue: 1.5, + floorRuleValue: 1, + floorRule: '12345/entertainment|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 2.00, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + matchedFields: { + gptSlot: '12345/entertainment', + mediaType: 'banner' + } + } + }; + + let bidRequest = utils.deepClone(MOCK.BID_REQUESTED); + bidRequest.bids.push(floorMinRequest) + // spoof the auction with just our duplicates events.emit(AUCTION_INIT, auctionInit); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_REQUESTED, bidRequest); events.emit(BID_RESPONSE, flooredResponse); events.emit(BID_RESPONSE, notFlooredResponse); + events.emit(BID_RESPONSE, floorMinResponse); events.emit(AUCTION_END, MOCK.AUCTION_END); events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(BID_WON, MOCK.BID_WON[2]); clock.tick(SEND_TIMEOUT + 1000); expect(server.requests.length).to.equal(1); @@ -860,7 +946,7 @@ describe('rubicon analytics adapter', function () { } it('should capture price floor information correctly', function () { - let message = performFloorAuction('rubicon') + let message = performFloorAuction('rubicon'); // verify our floor stuff is passed // top level floor info @@ -892,6 +978,16 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); + + // second adUnit's adSlot + expect(message.auctions[0].adUnits[2].gam.adSlot).to.equal('12345/entertainment'); + // top level adUnit status is success + expect(message.auctions[0].adUnits[2].status).to.equal('success'); + // second adUnits bid is success + expect(message.auctions[0].adUnits[2].bids[0].status).to.equal('success'); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorValue).to.equal(1.5); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorRuleValue).to.equal(1); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.bidPriceUSD).to.equal(2.01); }); it('should still send floor info if provider is not rubicon', function () { @@ -927,6 +1023,16 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); + + // second adUnit's adSlot + expect(message.auctions[0].adUnits[2].gam.adSlot).to.equal('12345/entertainment'); + // top level adUnit status is success + expect(message.auctions[0].adUnits[2].status).to.equal('success'); + // second adUnits bid is success + expect(message.auctions[0].adUnits[2].bids[0].status).to.equal('success'); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorValue).to.equal(1.5); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorRuleValue).to.equal(1); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.bidPriceUSD).to.equal(2.01); }); describe('with session handling', function () { @@ -977,6 +1083,34 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); + it('should use the query utm param rubicon kv value and pass updated kv and pvid when defined', function () { + sandbox.stub(utils, 'getWindowLocation').returns({'search': '?utm_source=other', 'pbjs_debug': 'true'}); + + config.setConfig({rubicon: { + fpkvs: { + source: 'fb', + link: 'email' + } + }}); + performStandardAuction(); + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + validate(message); + + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + expectedMessage.session.pvid = STUBBED_UUID.slice(0, 8); + expectedMessage.fpkvs = [ + {key: 'source', value: 'other'}, + {key: 'link', value: 'email'} + ] + + message.fpkvs.sort((left, right) => left.key < right.key); + expectedMessage.fpkvs.sort((left, right) => left.key < right.key); + + expect(message).to.deep.equal(expectedMessage); + }); + it('should pick up existing localStorage and use its values', function () { // set some localStorage let inputlocalStorage = { @@ -1029,6 +1163,65 @@ describe('rubicon analytics adapter', function () { }); }); + it('should overwrite matching localstorge value and use its remaining values', function () { + sandbox.stub(utils, 'getWindowLocation').returns({'search': '?utm_source=fb&utm_click=dog'}); + + // set some localStorage + let inputlocalStorage = { + id: '987654', + start: 1519766113781, // 15 mins before "now" + expires: 1519787713781, // six hours later + lastSeen: 1519766113781, + fpkvs: { source: 'tw', link: 'email' } + }; + getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + config.setConfig({rubicon: { + fpkvs: { + link: 'email' // should merge this with what is in the localStorage! + } + }}); + performStandardAuction(); + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + validate(message); + + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + expectedMessage.session = { + id: '987654', + start: 1519766113781, + expires: 1519787713781, + pvid: expectedPvid + } + expectedMessage.fpkvs = [ + {key: 'source', value: 'fb'}, + {key: 'link', value: 'email'}, + {key: 'click', value: 'dog'} + ] + + message.fpkvs.sort((left, right) => left.key < right.key); + expectedMessage.fpkvs.sort((left, right) => left.key < right.key); + + expect(message).to.deep.equal(expectedMessage); + + let calledWith; + try { + calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); + } catch (e) { + calledWith = {}; + } + + expect(calledWith).to.deep.equal({ + id: '987654', // should have stayed same + start: 1519766113781, // should have stayed same + expires: 1519787713781, // should have stayed same + lastSeen: 1519767013781, // lastSeen updated to our "now" + fpkvs: { source: 'fb', link: 'email', click: 'dog' }, // link merged in + pvid: expectedPvid // new pvid stored + }); + }); + it('should throw out session if lastSeen > 30 mins ago and create new one', function () { // set some localStorage let inputlocalStorage = { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index a38743d634a..6944034a787 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -246,7 +246,7 @@ describe('the rubicon adapter', function () { id: '4444444' }] }], - criteoId: '1111' + criteoId: '1111', }; bid.userIdAsEids = createEidsArray(bid.userId); bid.storedAuctionResponse = 11111; @@ -396,7 +396,10 @@ describe('the rubicon adapter', function () { describe('to fastlane', function () { it('should make a well-formed request object', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let duplicate = Object.assign(bidderRequest); + duplicate.bids[0].params.floor = 0.01; + + let [request] = spec.buildRequests(duplicate.bids, duplicate); let data = parseQuery(request.data); expect(request.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); @@ -551,7 +554,7 @@ describe('the rubicon adapter', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_floor', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'slots', 'rand']; + const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'slots', 'rand']; request.data.split('&').forEach((item, i) => { expect(item.split('=')[0]).to.equal(referenceOrdering[i]); @@ -566,7 +569,6 @@ describe('the rubicon adapter', function () { 'size_id': '15', 'alt_size_ids': '43', 'p_pos': 'atf', - 'rp_floor': '0.01', 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, @@ -883,7 +885,6 @@ describe('the rubicon adapter', function () { 'size_id': '15', 'alt_size_ids': '43', 'p_pos': 'atf', - 'rp_floor': '0.01', 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, @@ -1097,6 +1098,7 @@ describe('the rubicon adapter', function () { let data = parseQuery(request.data); expect(data['tpid_tdid']).to.equal('abcd-efgh-ijkl-mnop-1234'); + expect(data['eid_adserver.org']).to.equal('abcd-efgh-ijkl-mnop-1234'); }); describe('LiveIntent support', function () { @@ -1104,7 +1106,8 @@ describe('the rubicon adapter', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { lipb: { - lipbid: '0000-1111-2222-3333' + lipbid: '0000-1111-2222-3333', + segments: ['segA', 'segB'] } }; clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); @@ -1112,6 +1115,8 @@ describe('the rubicon adapter', function () { let data = parseQuery(request.data); expect(data['tpid_liveintent.com']).to.equal('0000-1111-2222-3333'); + expect(data['eid_liveintent.com']).to.equal('0000-1111-2222-3333'); + expect(data['tg_v.LIseg']).to.equal('segA,segB'); }); it('should send tg_v.LIseg when userIdAsEids contains liveintentId with ext.segments as array', function () { @@ -1217,6 +1222,43 @@ describe('the rubicon adapter', function () { }); }); + describe('ID5 support', function () { + it('should send ID5 id when userIdAsEids contains ID5', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + id5id: { + uid: '11111', + ext: { + linkType: '22222' + } + } + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_id5-sync.com']).to.equal('11111^1^22222'); + }); + }); + + describe('UserID catchall support', function () { + it('should send user id with generic format', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + // Hardcoding userIdAsEids since createEidsArray returns empty array if source not found in eids.js + clonedBid.userIdAsEids = [{ + source: 'catchall', + uids: [{ + id: '11111', + atype: 2 + }] + }] + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_catchall']).to.equal('11111^2'); + }); + }); + describe('Config user.id support', function () { it('should send ppuid when config defines user.id', function () { config.setConfig({user: {id: '123'}}); @@ -1429,20 +1471,10 @@ describe('the rubicon adapter', function () { 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.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'); @@ -2016,7 +2048,6 @@ describe('the rubicon adapter', function () { 'size_id': 15, 'alt_size_ids': '43', 'p_pos': 'atf', - 'rp_floor': 0.01, 'rp_secure': /[01]/, 'tk_flint': INTEGRATION, 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', @@ -2127,6 +2158,7 @@ describe('the rubicon adapter', function () { 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', 'size_id': '15', 'ad_id': '6', + 'adomain': ['test.com'], 'advertiser': 7, 'network': 8, 'creative_id': 'crid-9', @@ -2148,6 +2180,7 @@ describe('the rubicon adapter', function () { 'impression_id': '153dc240-8229-4604-b8f5-256933b9374d', 'size_id': '43', 'ad_id': '7', + 'adomain': ['test.com'], 'advertiser': 7, 'network': 8, 'creative_id': 'crid-9', @@ -2182,6 +2215,8 @@ describe('the rubicon adapter', function () { expect(bids[0].rubicon.networkId).to.equal(8); expect(bids[0].creativeId).to.equal('crid-9'); expect(bids[0].currency).to.equal('USD'); + expect(bids[0].meta.mediaType).to.equal('banner'); + expect(String(bids[0].meta.advertiserDomains)).to.equal('test.com'); expect(bids[0].ad).to.contain(`alert('foo')`) .and.to.contain(``) .and.to.contain(`
`); @@ -2715,13 +2750,15 @@ describe('the rubicon adapter', function () { bid: [{ id: '0', impid: 'instream_video1', + adomain: ['test.com'], price: 2, crid: '4259970', ext: { bidder: { rp: { mime: 'application/javascript', - size_id: 201 + size_id: 201, + advid: 12345 } }, prebid: { @@ -2750,6 +2787,9 @@ describe('the rubicon adapter', function () { expect(bids[0].netRevenue).to.equal(true); expect(bids[0].adserverTargeting).to.deep.equal({hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64'}); expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].meta.mediaType).to.equal('video'); + expect(String(bids[0].meta.advertiserDomains)).to.equal('test.com'); + expect(bids[0].meta.advertiserId).to.equal(12345); expect(bids[0].bidderCode).to.equal('rubicon'); expect(bids[0].currency).to.equal('USD'); expect(bids[0].width).to.equal(640); diff --git a/test/spec/modules/shareUserIds_spec.js b/test/spec/modules/shareUserIds_spec.js index 451892919cb..67e39533fc7 100644 --- a/test/spec/modules/shareUserIds_spec.js +++ b/test/spec/modules/shareUserIds_spec.js @@ -50,6 +50,16 @@ describe('#userIdTargeting', function() { pubads.setTargeting('test', ['TEST']); config.GAM_KEYS.tdid = ''; userIdTargeting(userIds, config); + expect(pubads.getTargeting('tdid')).to.be.an('array').that.is.empty; expect(pubads.getTargeting('test')).to.deep.equal(['TEST']); }); + + it('User Id Targeting is added to googletag queue when GPT is not ready', function() { + let pubads = window.googletag.pubads; + delete window.googletag.pubads; + userIdTargeting(userIds, config); + window.googletag.pubads = pubads; + window.googletag.cmd.map(command => command()); + expect(window.googletag.pubads().getTargeting('TD_ID')).to.deep.equal(['my-tdid']); + }); }); diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 92f9fd11eeb..d45d1e977e6 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -12,7 +12,13 @@ const bidRequests = [ params: { pkey: 'aaaa1111' }, - userId: { tdid: 'fake-tdid' } + userId: { + tdid: 'fake-tdid', + pubcid: 'fake-pubcid' + }, + crumbs: { + pubcid: 'fake-pubcid-in-crumbs-obj' + } }, { bidder: 'sharethrough', @@ -33,8 +39,8 @@ const bidRequests = [ pkey: 'cccc3333', iframe: true, iframeSize: [500, 500] - }, - }, + } + } ]; const prebidRequests = [ @@ -98,7 +104,7 @@ const prebidRequests = [ skipIframeBusting: false, sizes: [[300, 250], [300, 300], [250, 250], [600, 50]] } - }, + } ]; const bidderResponse = { @@ -121,12 +127,12 @@ const bidderResponse = { }; const setUserAgent = (uaString) => { - window.navigator['__defineGetter__']('userAgent', function () { + window.navigator['__defineGetter__']('userAgent', function() { return uaString; }); }; -describe('sharethrough internal spec', function () { +describe('sharethrough internal spec', function() { let windowSpy, windowTopSpy; beforeEach(function() { @@ -141,7 +147,7 @@ describe('sharethrough internal spec', function () { window.top.STR = undefined; }); - describe('we cannot access top level document', function () { + describe('we cannot access top level document', function() { beforeEach(function() { window.lockedInFrame = true; }); @@ -150,12 +156,12 @@ describe('sharethrough internal spec', function () { window.lockedInFrame = false; }); - it('appends sfp.js to the safeframe', function () { + it('appends sfp.js to the safeframe', function() { sharethroughInternal.handleIframe(); expect(windowSpy.calledOnce).to.be.true; }); - it('does not append anything if sfp.js is already loaded in the safeframe', function () { + it('does not append anything if sfp.js is already loaded in the safeframe', function() { window.STR = { Tag: true }; sharethroughInternal.handleIframe(); expect(windowSpy.notCalled).to.be.true; @@ -163,14 +169,14 @@ describe('sharethrough internal spec', function () { }); }); - describe('we are able to bust out of the iframe', function () { - it('appends sfp.js to window.top', function () { + describe('we are able to bust out of the iframe', function() { + it('appends sfp.js to window.top', function() { sharethroughInternal.handleIframe(); expect(windowSpy.calledOnce).to.be.true; expect(windowTopSpy.calledOnce).to.be.true; }); - it('only appends sfp-set-targeting.js if sfp.js is already loaded on the page', function () { + it('only appends sfp-set-targeting.js if sfp.js is already loaded on the page', function() { window.top.STR = { Tag: true }; sharethroughInternal.handleIframe(); expect(windowSpy.calledOnce).to.be.true; @@ -179,15 +185,15 @@ describe('sharethrough internal spec', function () { }); }); -describe('sharethrough adapter spec', function () { - describe('.code', function () { - it('should return a bidder code of sharethrough', function () { +describe('sharethrough adapter spec', function() { + describe('.code', function() { + it('should return a bidder code of sharethrough', function() { expect(spec.code).to.eql('sharethrough'); }); }); - describe('.isBidRequestValid', function () { - it('should return false if req has no pkey', function () { + describe('.isBidRequestValid', function() { + it('should return false if req has no pkey', function() { const invalidBidRequest = { bidder: 'sharethrough', params: { @@ -197,7 +203,7 @@ describe('sharethrough adapter spec', function () { expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); - it('should return false if req has wrong bidder code', function () { + it('should return false if req has wrong bidder code', function() { const invalidBidRequest = { bidder: 'notSharethrough', params: { @@ -207,14 +213,14 @@ describe('sharethrough adapter spec', function () { expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); - it('should return true if req is correct', function () { + it('should return true if req is correct', function() { expect(spec.isBidRequestValid(bidRequests[0])).to.eq(true); expect(spec.isBidRequestValid(bidRequests[1])).to.eq(true); - }) + }); }); - describe('.buildRequests', function () { - it('should return an array of requests', function () { + describe('.buildRequests', function() { + it('should return an array of requests', function() { const builtBidRequests = spec.buildRequests(bidRequests); expect(builtBidRequests[0].url).to.eq('https://btlr.sharethrough.com/WYu2BXv1/v1'); @@ -222,7 +228,7 @@ describe('sharethrough adapter spec', function () { expect(builtBidRequests[0].method).to.eq('GET'); }); - it('should set the instant_play_capable parameter correctly based on browser userAgent string', function () { + it('should set the instant_play_capable parameter correctly based on browser userAgent string', function() { setUserAgent('Android Chrome/60'); let builtBidRequests = spec.buildRequests(bidRequests); expect(builtBidRequests[0].data.instant_play_capable).to.be.true; @@ -252,31 +258,31 @@ describe('sharethrough adapter spec', function () { const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('http:'); const bidRequest = spec.buildRequests(bidRequests, null)[0]; expect(bidRequest.data.secure).to.be.false; - stub.restore() + stub.restore(); }); it('should set the secure parameter to true when the protocol is https', function() { const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('https:'); const bidRequest = spec.buildRequests(bidRequests, null)[0]; expect(bidRequest.data.secure).to.be.true; - stub.restore() + stub.restore(); }); it('should set the secure parameter to true when the protocol is neither http or https', function() { const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('about:'); const bidRequest = spec.buildRequests(bidRequests, null)[0]; expect(bidRequest.data.secure).to.be.true; - stub.restore() + stub.restore(); }); - it('should add ccpa parameter if uspConsent is present', function () { + it('should add ccpa parameter if uspConsent is present', function() { const uspConsent = '1YNN'; const bidderRequest = { uspConsent: uspConsent }; const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; expect(bidRequest.data.us_privacy).to.eq(uspConsent); }); - it('should add consent parameters if gdprConsent is present', function () { + it('should add consent parameters if gdprConsent is present', function() { const gdprConsent = { consentString: 'consent_string123', gdprApplies: true }; const bidderRequest = { gdprConsent: gdprConsent }; const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; @@ -284,19 +290,32 @@ describe('sharethrough adapter spec', function () { expect(bidRequest.data.consent_string).to.eq('consent_string123'); }); - it('should handle gdprConsent is present but values are undefined case', function () { + it('should handle gdprConsent is present but values are undefined case', function() { const gdprConsent = { consent_string: undefined, gdprApplies: undefined }; const bidderRequest = { gdprConsent: gdprConsent }; const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(bidRequest.data).to.not.include.any.keys('consent_string') + expect(bidRequest.data).to.not.include.any.keys('consent_string'); }); - it('should add the ttduid parameter if a bid request contains a value for Unified ID from The Trade Desk', function () { + it('should add the ttduid parameter if a bid request contains a value for Unified ID from The Trade Desk', function() { const bidRequest = spec.buildRequests(bidRequests)[0]; expect(bidRequest.data.ttduid).to.eq('fake-tdid'); }); - it('should add Sharethrough specific parameters', function () { + it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + + ' userId object of the bidrequest', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); + }); + + it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + + ' crumbs object of the bidrequest', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + delete bidRequest.userId; + expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); + }); + + it('should add Sharethrough specific parameters', function() { const builtBidRequests = spec.buildRequests(bidRequests); expect(builtBidRequests[0]).to.deep.include({ strData: { @@ -346,8 +365,8 @@ describe('sharethrough adapter spec', function () { }); }); - describe('.interpretResponse', function () { - it('returns a correctly parsed out response', function () { + describe('.interpretResponse', function() { + it('returns a correctly parsed out response', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[0])[0]).to.include( { width: 1, @@ -357,11 +376,11 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a correctly parsed out response with largest size when strData.skipIframeBusting is true', function () { + it('returns a correctly parsed out response with largest size when strData.skipIframeBusting is true', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[1])[0]).to.include( { width: 300, @@ -371,11 +390,11 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is true and strData.iframeSize is provided', function () { + it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is true and strData.iframeSize is provided', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[2])[0]).to.include( { width: 500, @@ -385,11 +404,11 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains [0, 0] only', function () { + it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains [0, 0] only', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[3])[0]).to.include( { width: 0, @@ -399,11 +418,11 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains multiple sizes', function () { + it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains multiple sizes', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[4])[0]).to.include( { width: 300, @@ -413,26 +432,26 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a blank array if there are no creatives', function () { + it('returns a blank array if there are no creatives', function() { const bidResponse = { body: { creatives: [] } }; expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); - it('returns a blank array if body object is empty', function () { + it('returns a blank array if body object is empty', function() { const bidResponse = { body: {} }; expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); - it('returns a blank array if body is null', function () { + it('returns a blank array if body is null', function() { const bidResponse = { body: null }; expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); - it('correctly generates ad markup when skipIframeBusting is false', function () { + it('correctly generates ad markup when skipIframeBusting is false', function() { const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[0])[0].ad; let resp = null; @@ -447,7 +466,7 @@ describe('sharethrough adapter spec', function () { expect(adMarkup).to.match(/handleIframe/); }); - it('correctly generates ad markup when skipIframeBusting is true', function () { + it('correctly generates ad markup when skipIframeBusting is true', function() { const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[1])[0].ad; let resp = null; @@ -461,11 +480,11 @@ describe('sharethrough adapter spec', function () { }); }); - describe('.getUserSyncs', function () { + describe('.getUserSyncs', function() { const cookieSyncs = ['cookieUrl1', 'cookieUrl2', 'cookieUrl3']; const serverResponses = [{ body: { cookieSyncUrls: cookieSyncs } }]; - it('returns an array of correctly formatted user syncs', function () { + it('returns an array of correctly formatted user syncs', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, serverResponses, null, 'fake-privacy-signal'); expect(syncArray).to.deep.equal([ { type: 'image', url: 'cookieUrl1&us_privacy=fake-privacy-signal' }, @@ -474,22 +493,22 @@ describe('sharethrough adapter spec', function () { ); }); - it('returns an empty array if serverResponses is empty', function () { + it('returns an empty array if serverResponses is empty', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, []); expect(syncArray).to.be.an('array').that.is.empty; }); - it('returns an empty array if the body is null', function () { + it('returns an empty array if the body is null', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, [{ body: null }]); expect(syncArray).to.be.an('array').that.is.empty; }); - it('returns an empty array if the body.cookieSyncUrls is missing', function () { + it('returns an empty array if the body.cookieSyncUrls is missing', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, [{ body: { creatives: ['creative'] } }]); expect(syncArray).to.be.an('array').that.is.empty; }); - it('returns an empty array if pixels are not enabled', function () { + it('returns an empty array if pixels are not enabled', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: false }, serverResponses); expect(syncArray).to.be.an('array').that.is.empty; }); diff --git a/test/spec/modules/smaatoBidAdapter_spec.js b/test/spec/modules/smaatoBidAdapter_spec.js index 13716b51436..6af0a855800 100644 --- a/test/spec/modules/smaatoBidAdapter_spec.js +++ b/test/spec/modules/smaatoBidAdapter_spec.js @@ -1,6 +1,7 @@ import { spec } from 'modules/smaatoBidAdapter.js'; import * as utils from 'src/utils.js'; import {config} from 'src/config.js'; +import {createEidsArray} from 'modules/userId/eids.js'; const imageAd = { image: { @@ -321,6 +322,37 @@ const inAppBidRequest = { bidderWinsCount: 0 }; +const userIdBidRequest = { + bidder: 'smaato', + params: { + publisherId: 'publisherId', + adspaceId: 'adspaceId' + }, + mediaTypes: { + banner: { + sizes: [[300, 50]] + } + }, + adUnitCode: '/19968336/header-bid-tag-0', + transactionId: 'transactionId', + sizes: [[300, 50]], + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + userId: { + criteoId: '123456', + tdid: '89145' + }, + userIdAsEids: createEidsArray({ + criteoId: '123456', + tdid: '89145' + }) +}; + describe('smaatoBidAdapterTest', () => { describe('isBidRequestValid', () => { it('has valid params', () => { @@ -422,7 +454,11 @@ describe('smaatoBidAdapterTest', () => { expect(req_fpd.user.ext.consent).to.equal('HFIDUYFIUYIUYWIPOI87392DSU'); expect(req_fpd.site.keywords).to.eql('power tools,drills'); expect(req_fpd.site.publisher.id).to.equal('publisherId'); - }) + }); + + it('has no user ids', () => { + expect(this.req.user.ext.eids).to.not.exist; + }); }); describe('buildRequests for video imps', () => { @@ -513,6 +549,14 @@ describe('smaatoBidAdapterTest', () => { }); }); + describe('user ids in requests', () => { + it('user ids are added to user.ext.eids', () => { + let req = JSON.parse(spec.buildRequests([userIdBidRequest], defaultBidderRequest).data); + expect(req.user.ext.eids).to.exist; + expect(req.user.ext.eids).to.have.length(2); + }); + }); + describe('interpretResponse', () => { it('single image reponse', () => { const bids = spec.interpretResponse(openRtbBidResponse(ADTYPE_IMG), request); diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 321fed40d83..983ade4dd14 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -331,16 +331,16 @@ describe('sovrnBidAdapter', function() { 'currency': 'USD', 'netRevenue': true, 'mediaType': 'banner', - 'ad': decodeURIComponent(`>`), - 'ttl': 60000 + 'ad': decodeURIComponent(``), + 'ttl': 90 }]; let result = spec.interpretResponse(response); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + expect(result[0]).to.deep.equal(expectedResponse[0]); }); it('should get correct bid response when dealId is passed', function () { - response.body.dealid = 'baking'; + response.body.seatbid[0].bid[0].dealid = 'baking'; let expectedResponse = [{ 'requestId': '263c448586f5a1', @@ -352,12 +352,33 @@ describe('sovrnBidAdapter', function() { 'currency': 'USD', 'netRevenue': true, 'mediaType': 'banner', - 'ad': decodeURIComponent(`>`), - 'ttl': 60000 + 'ad': decodeURIComponent(``), + 'ttl': 90 }]; let result = spec.interpretResponse(response); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + expect(result[0]).to.deep.equal(expectedResponse[0]); + }); + + it('should get correct bid response when ttl is set', function () { + response.body.seatbid[0].bid[0].ttl = 480; + + let expectedResponse = [{ + 'requestId': '263c448586f5a1', + 'cpm': 0.45882675, + 'width': 728, + 'height': 90, + 'creativeId': 'creativelycreatedcreativecreative', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': decodeURIComponent(``), + 'ttl': 480 + }]; + + let result = spec.interpretResponse(response); + expect(result[0]).to.deep.equal(expectedResponse[0]); }); it('handles empty bid response', function () { diff --git a/test/spec/modules/sspBCAdapter_spec.js b/test/spec/modules/sspBCAdapter_spec.js index 2cb0e8defa4..29718deb031 100644 --- a/test/spec/modules/sspBCAdapter_spec.js +++ b/test/spec/modules/sspBCAdapter_spec.js @@ -1,6 +1,8 @@ import { assert, expect } from 'chai'; import { spec } from 'modules/sspBCAdapter.js'; import * as utils from 'src/utils.js'; +import * as sinon from 'sinon'; +import * as ajax from 'src/ajax.js'; const BIDDER_CODE = 'sspBC'; const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; @@ -60,10 +62,33 @@ describe('SSPBC adapter', function () { }, auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: auctionId + '2', transactionId, } ]; + const bids_timeouted = [{ + adUnitCode: 'test_wideboard', + bidder: BIDDER_CODE, + params: [{ + id: '003', + siteId: '8816', + }], + auctionId, + bidId: auctionId + '1', + timeout: 100, + }, + { + adUnitCode: 'test_rectangle', + bidder: BIDDER_CODE, + params: [{ + id: '005', + siteId: '8816', + }], + auctionId, + bidId: auctionId + '2', + timeout: 100, + } + ]; const bids_test = [{ adUnitCode: 'test_wideboard', bidder: BIDDER_CODE, @@ -209,6 +234,7 @@ describe('SSPBC adapter', function () { return { bids, bids_test, + bids_timeouted, bidRequest, bidRequestSingle, bidRequestTest, @@ -323,7 +349,7 @@ describe('SSPBC adapter', function () { 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); + expect(syncResultAll[0].url).to.have.string(SYNC_URL); }); it('should send no syncs, if frame sync is not allowed', function () { @@ -331,4 +357,44 @@ describe('SSPBC adapter', function () { expect(syncResultNone).to.be.undefined; }); }); + + describe('onBidWon', function () { + it('should generate no notification if bid is undefined', function () { + let notificationPayload = spec.onBidWon(); + expect(notificationPayload).to.be.undefined; + }); + + it('should generate notification with event name and request/site/slot data, if correct bid is provided', function () { + const { bids } = prepareTestData(); + let bid = bids[0]; + bid.params = [bid.params]; + + let notificationPayload = spec.onBidWon(bid); + expect(notificationPayload).to.have.property('event').that.equals('bidWon'); + expect(notificationPayload).to.have.property('requestId').that.equals(bid.auctionId); + expect(notificationPayload).to.have.property('siteId').that.deep.equals([bid.params[0].siteId]); + expect(notificationPayload).to.have.property('slotId').that.deep.equals([bid.params[0].id]); + }); + }); + + describe('onTimeout', function () { + it('should generate no notification if timeout data is undefined / has no bids', function () { + let notificationPayloadUndefined = spec.onTimeout(); + let notificationPayloadNoBids = spec.onTimeout([]); + + expect(notificationPayloadUndefined).to.be.undefined; + expect(notificationPayloadNoBids).to.be.undefined; + }); + + it('should generate single notification for any number of timeouted bids', function () { + const { bids_timeouted } = prepareTestData(); + + let notificationPayload = spec.onTimeout(bids_timeouted); + + expect(notificationPayload).to.have.property('event').that.equals('timeout'); + expect(notificationPayload).to.have.property('requestId').that.equals(bids_timeouted[0].auctionId); + expect(notificationPayload).to.have.property('siteId').that.deep.equals([bids_timeouted[0].params[0].siteId]); + expect(notificationPayload).to.have.property('slotId').that.deep.equals([bids_timeouted[0].params[0].id, bids_timeouted[1].params[0].id]); + }); + }); }); diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index 797b3fab0c1..b417876f276 100644 --- a/test/spec/modules/tripleliftBidAdapter_spec.js +++ b/test/spec/modules/tripleliftBidAdapter_spec.js @@ -83,15 +83,21 @@ describe('triplelift adapter', function () { expect(tripleliftAdapterSpec.isBidRequestValid(instreamBid)).to.equal(true); }); + it('should return true when required params found - instream - 2', function () { + delete instreamBid.mediaTypes.playerSize; + delete instreamBid.params.video.w; + delete instreamBid.params.video.h; + // the only required param is inventoryCode + expect(tripleliftAdapterSpec.isBidRequestValid(instreamBid)).to.equal(true); + }); + it('should return false when required params are not passed', function () { delete bid.params.inventoryCode; expect(tripleliftAdapterSpec.isBidRequestValid(bid)).to.equal(false); }); it('should return false when required params are not passed - instream', function () { - delete instreamBid.mediaTypes.playerSize; - delete instreamBid.params.video.w; - delete instreamBid.params.video.h; + delete instreamBid.params.inventoryCode; expect(tripleliftAdapterSpec.isBidRequestValid(instreamBid)).to.equal(false); }); }); @@ -164,6 +170,174 @@ describe('triplelift adapter', function () { auctionId: '1d1a030790a475', userId: {}, schain, + }, + // banner and outstream video + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + }, + banner: { + sizes: [ + [970, 250], + [1, 1] + ] + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // banner and incomplete video + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + + }, + banner: { + sizes: [ + [970, 250], + [1, 1] + ] + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // incomplete banner and incomplete video + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + + }, + banner: { + + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // banner and instream video + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480] + }, + banner: { + sizes: [ + [970, 250], + [1, 1] + ] + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // banner and outream video and native + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + }, + banner: { + sizes: [ + [970, 250], + [1, 1] + ] + }, + native: { + + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, } ]; @@ -228,6 +402,26 @@ describe('triplelift adapter', function () { expect(payload.imp[1].tagid).to.equal('insteam_test'); expect(payload.imp[1].floor).to.equal(1.0); expect(payload.imp[1].video).to.exist.and.to.be.a('object'); + // banner and outstream video + expect(payload.imp[2]).to.not.have.property('video'); + expect(payload.imp[2]).to.have.property('banner'); + expect(payload.imp[2].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + // banner and incomplete video + expect(payload.imp[3]).to.not.have.property('video'); + expect(payload.imp[3]).to.have.property('banner'); + expect(payload.imp[3].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + // incomplete mediatypes.banner and incomplete video + expect(payload.imp[4]).to.not.have.property('video'); + expect(payload.imp[4]).to.have.property('banner'); + expect(payload.imp[4].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + // banner and instream video + expect(payload.imp[5]).to.not.have.property('banner'); + expect(payload.imp[5]).to.have.property('video'); + expect(payload.imp[5].video).to.exist.and.to.be.a('object'); + // banner and outream video and native + expect(payload.imp[6]).to.not.have.property('video'); + expect(payload.imp[6]).to.have.property('banner'); + expect(payload.imp[6].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); }); it('should add tdid to the payload if included', function () { @@ -444,7 +638,8 @@ describe('triplelift adapter', function () { ad: 'ad-markup', iurl: 'https://s.adroll.com/a/IYR/N36/IYRN366MFVDITBAGNNT5U6.jpg', tl_source: 'tlx', - advertiser_name: 'fake advertiser name' + advertiser_name: 'fake advertiser name', + adomain: ['basspro.com', 'internetalerts.org'] }, { imp_id: 1, @@ -550,9 +745,16 @@ describe('triplelift adapter', function () { it('should include the advertiser name in the meta field if available', function () { let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); - expect(result[0].meta.advertiserName).to.equal('fake advertiser name') + expect(result[0].meta.advertiserName).to.equal('fake advertiser name'); expect(result[1].meta).to.not.have.key('advertiserName'); }); + + it('should include the advertiser domain array in the meta field if available', function () { + let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); + expect(result[0].meta.advertiserDomains[0]).to.equal('basspro.com'); + expect(result[0].meta.advertiserDomains[1]).to.equal('internetalerts.org'); + expect(result[1].meta).to.not.have.key('advertiserDomains'); + }); }); describe('getUserSyncs', function() { diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index c5ab2e249fc..981ebb5f50e 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -7,7 +7,8 @@ import { setStoredConsentData, setStoredValue, setSubmoduleRegistry, - syncDelay + syncDelay, + PBJS_USER_ID_OPTOUT_NAME } from 'modules/userId/index.js'; import {createEidsArray} from 'modules/userId/eids.js'; import {config} from 'src/config.js'; @@ -91,9 +92,7 @@ describe('User ID', function () { } before(function () { - coreStorage.setCookie('_pubcid_optout', '', EXPIRED_COOKIE_DATE); - localStorage.removeItem('_pbjs_id_optout'); - localStorage.removeItem('_pubcid_optout'); + localStorage.removeItem(PBJS_USER_ID_OPTOUT_NAME); }); beforeEach(function () { @@ -413,7 +412,7 @@ describe('User ID', function () { describe('Opt out', function () { before(function () { - coreStorage.setCookie('_pbjs_id_optout', '1', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie(PBJS_USER_ID_OPTOUT_NAME, '1', (new Date(Date.now() + 5000).toUTCString())); }); beforeEach(function () { @@ -422,16 +421,12 @@ describe('User ID', function () { afterEach(function () { // removed cookie - coreStorage.setCookie('_pbjs_id_optout', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie(PBJS_USER_ID_OPTOUT_NAME, '', EXPIRED_COOKIE_DATE); $$PREBID_GLOBAL$$.requestBids.removeAll(); utils.logInfo.restore(); config.resetConfig(); }); - after(function () { - coreStorage.setCookie('_pbjs_id_optout', '', EXPIRED_COOKIE_DATE); - }); - it('fails initialization if opt out cookie exists', function () { setSubmoduleRegistry([pubCommonIdSubmodule]); init(config); @@ -443,7 +438,7 @@ describe('User ID', function () { setSubmoduleRegistry([pubCommonIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 1 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 1 submodules'); }); }); @@ -506,7 +501,7 @@ describe('User ID', function () { init(config); config.setConfig(getConfigMock(['unifiedId', 'unifiedid', 'cookie'])); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 1 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 1 submodules'); }); it('config with 13 configurations should result in 13 submodules add', function () { @@ -553,7 +548,7 @@ describe('User ID', function () { }] } }); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 13 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 13 submodules'); }); it('config syncDelay updates module correctly', function () { @@ -611,6 +606,7 @@ describe('User ID', function () { beforeEach(function () { sandbox = sinon.createSandbox(); sandbox.stub(global, 'setTimeout').returns(2); + sandbox.stub(global, 'clearTimeout'); sandbox.stub(events, 'on'); sandbox.stub(coreStorage, 'getCookie'); @@ -662,6 +658,7 @@ describe('User ID', function () { requestBidsHook(auctionSpy, {adUnits}); // check auction was delayed + global.clearTimeout.calledOnce.should.equal(false); global.setTimeout.calledOnce.should.equal(true); global.setTimeout.calledWith(sinon.match.func, 33); auctionSpy.calledOnce.should.equal(false); @@ -696,6 +693,7 @@ describe('User ID', function () { // check auction was delayed // global.setTimeout.calledOnce.should.equal(true); + global.clearTimeout.calledOnce.should.equal(false); global.setTimeout.calledWith(sinon.match.func, 33); auctionSpy.calledOnce.should.equal(false); diff --git a/test/spec/modules/verizonMediaIdSystem_spec.js b/test/spec/modules/verizonMediaIdSystem_spec.js new file mode 100644 index 00000000000..a30be5a2569 --- /dev/null +++ b/test/spec/modules/verizonMediaIdSystem_spec.js @@ -0,0 +1,182 @@ +import {expect} from 'chai'; +import * as utils from 'src/utils.js'; +import {verizonMediaIdSubmodule} from 'modules/verizonMediaIdSystem.js'; + +describe('Verizon Media ID Submodule', () => { + const HASHED_EMAIL = '6bda6f2fa268bf0438b5423a9861a2cedaa5dec163c03f743cfe05c08a8397b2'; + const PIXEL_ID = '1234'; + const PROD_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PIXEL_ID}/fed`; + const OVERRIDE_ENDPOINT = 'https://foo/bar'; + + it('should have the correct module name declared', () => { + expect(verizonMediaIdSubmodule.name).to.equal('verizonMediaId'); + }); + + it('should have the correct TCFv2 Vendor ID declared', () => { + expect(verizonMediaIdSubmodule.gvlid).to.equal(25); + }); + + describe('getId()', () => { + let ajaxStub; + let getAjaxFnStub; + let consentData; + beforeEach(() => { + ajaxStub = sinon.stub(); + getAjaxFnStub = sinon.stub(verizonMediaIdSubmodule, 'getAjaxFn'); + getAjaxFnStub.returns(ajaxStub); + + consentData = { + gdpr: { + gdprApplies: 1, + consentString: 'GDPR_CONSENT_STRING' + }, + uspConsent: 'USP_CONSENT_STRING' + }; + }); + + afterEach(() => { + getAjaxFnStub.restore(); + }); + + function invokeGetIdAPI(configParams, consentData) { + let result = verizonMediaIdSubmodule.getId({ + params: configParams + }, consentData); + if (typeof result === 'object') { + result.callback(sinon.stub()); + } + return result; + } + + it('returns undefined if he and pixelId params are not passed', () => { + expect(invokeGetIdAPI({}, consentData)).to.be.undefined; + expect(ajaxStub.callCount).to.equal(0); + }); + + it('returns undefined if the pixelId param is not passed', () => { + expect(invokeGetIdAPI({ + he: HASHED_EMAIL + }, consentData)).to.be.undefined; + expect(ajaxStub.callCount).to.equal(0); + }); + + it('returns undefined if the he param is not passed', () => { + expect(invokeGetIdAPI({ + pixelId: PIXEL_ID + }, consentData)).to.be.undefined; + expect(ajaxStub.callCount).to.equal(0); + }); + + it('returns an object with the callback function if the correct params are passed', () => { + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + expect(result).to.be.an('object').that.has.all.keys('callback'); + expect(result.callback).to.be.a('function'); + }); + + it('Makes an ajax GET request to the production API endpoint with query params', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + const expectedParams = { + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + '1p': '0', + gdpr: '1', + euconsent: consentData.gdpr.consentString, + us_privacy: consentData.uspConsent + }; + const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + + expect(ajaxStub.firstCall.args[0].indexOf(`${PROD_ENDPOINT}?`)).to.equal(0); + expect(requestQueryParams).to.deep.equal(expectedParams); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); + }); + + it('Makes an ajax GET request to the specified override API endpoint with query params', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + endpoint: OVERRIDE_ENDPOINT + }, consentData); + + const expectedParams = { + he: HASHED_EMAIL, + '1p': '0', + gdpr: '1', + euconsent: consentData.gdpr.consentString, + us_privacy: consentData.uspConsent + }; + const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + + expect(ajaxStub.firstCall.args[0].indexOf(`${OVERRIDE_ENDPOINT}?`)).to.equal(0); + expect(requestQueryParams).to.deep.equal(expectedParams); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); + }); + + it('sets the callbacks param of the ajax function call correctly', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + expect(ajaxStub.firstCall.args[1]).to.be.an('object').that.has.all.keys(['success', 'error']); + }); + + it('sets GDPR consent data flag correctly when call is under GDPR jurisdiction.', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + expect(requestQueryParams.gdpr).to.equal('1'); + expect(requestQueryParams.euconsent).to.equal(consentData.gdpr.consentString); + }); + + it('sets GDPR consent data flag correctly when call is NOT under GDPR jurisdiction.', () => { + consentData.gdpr.gdprApplies = false; + + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + expect(requestQueryParams.gdpr).to.equal('0'); + expect(requestQueryParams.euconsent).to.equal(''); + }); + + [1, '1', true].forEach(firstPartyParamValue => { + it(`sets 1p payload property to '1' for a config value of ${firstPartyParamValue}`, () => { + invokeGetIdAPI({ + '1p': firstPartyParamValue, + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + expect(requestQueryParams['1p']).to.equal('1'); + }); + }); + }); + + describe('decode()', () => { + const VALID_API_RESPONSE = { + vmuid: '1234' + }; + it('should return a newly constructed object with the vmuid property', () => { + expect(verizonMediaIdSubmodule.decode(VALID_API_RESPONSE)).to.deep.equal(VALID_API_RESPONSE); + expect(verizonMediaIdSubmodule.decode(VALID_API_RESPONSE)).to.not.equal(VALID_API_RESPONSE); + }); + + [{}, '', {foo: 'bar'}].forEach((response) => { + it(`should return undefined for an invalid response "${JSON.stringify(response)}"`, () => { + expect(verizonMediaIdSubmodule.decode(response)).to.be.undefined; + }); + }); + }); +}); diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index 90fa26fa823..cd2c46a5664 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -138,6 +138,12 @@ describe('yieldlabBidAdapter', function () { } }) + it('passes unencoded schain string to bid request when complete == 0', function () { + REQUEST.schain.complete = 0; + const request = spec.buildRequests([REQUEST]) + expect(request.url).to.include('schain=1.0,0!indirectseller.com,1,1,,,,!indirectseller2.com,2,1,,indirectseller2%20name%20with%20comma%20%2C%20and%20bang%20%21,,') + }) + it('passes encoded referer to bid request', function () { expect(refererRequest.url).to.include('pubref=https%3A%2F%2Fwww.yieldlab.de%2Ftest%3Fwith%3Dquerystring') }) diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index caeb26266fe..deabef69093 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -1,19 +1,16 @@ import { expect } from 'chai'; import { spec } from 'modules/yieldmoBidAdapter.js'; -import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; describe('YieldmoAdapter', function () { - const adapter = newBidder(spec); - const ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; + const BANNER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; + const VIDEO_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebidvideo'; - let tdid = '8d146286-91d4-4958-aff4-7e489dd1abd6'; - let criteoId = 'aff4'; - - let bid = { + const mockBannerBid = (rootParams = {}, params = {}) => ({ bidder: 'yieldmo', params: { bidFloor: 0.1, + ...params, }, adUnitCode: 'adunit-code', mediaTypes: { @@ -31,15 +28,44 @@ describe('YieldmoAdapter', function () { pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da', }, userId: { - tdid, + tdid: '8d146286-91d4-4958-aff4-7e489dd1abd6' + }, + ...rootParams + }); + + const mockVideoBid = (rootParams = {}, params = {}, videoParams = {}) => ({ + bidder: 'yieldmo', + adUnitCode: 'adunit-code-video', + bidId: '321video123', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + mimes: ['video/mp4'] + }, }, - }; - let bidArray = [bid]; - let bidderRequest = { + params: { + placementId: '123', + ...params, + video: { + placement: 1, + maxduration: 30, + startdelay: 10, + protocols: [2, 3], + api: [2, 3], + skipppable: true, + playbackmethod: [1, 2], + ...videoParams + } + }, + ...rootParams + }); + + const mockBidderRequest = (params = {}, bids = [mockBannerBid()]) => ({ bidderCode: 'yieldmo', auctionId: 'e3a336ad-2761-4a1c-b421-ecc7c5294a34', bidderRequestId: '14c4ede8c693f', - bids: bidArray, + bids, auctionStart: 1520001292880, timeout: 3000, start: 1520001292884, @@ -49,236 +75,215 @@ describe('YieldmoAdapter', function () { reachedTop: true, referer: 'yieldmo.com', }, - }; + ...params + }); describe('isBidRequestValid', function () { - it('should return true when necessary information is found', function () { - expect(spec.isBidRequestValid(bid)).to.be.true; + describe('Banner:', function () { + it('should return true when necessary information is found', function () { + expect(spec.isBidRequestValid(mockBannerBid())).to.be.true; + }); + + it('should return false when necessary information is not found', function () { + // empty bid + expect(spec.isBidRequestValid({})).to.be.false; + + // empty bidId + expect(spec.isBidRequestValid(mockBannerBid({bidId: ''}))).to.be.false; + + // empty adUnitCode + expect(spec.isBidRequestValid(mockBannerBid({adUnitCode: ''}))).to.be.false; + + let invalidBid = mockBannerBid(); + delete invalidBid.mediaTypes.banner; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); }); - it('should return false when necessary information is not found', function () { - // empty bid - expect(spec.isBidRequestValid({})).to.be.false; + describe('Instream video:', function () { + const getVideoBidWithoutParam = (key, paramToRemove) => { + let bid = mockVideoBid(); + delete utils.deepAccess(bid, key)[paramToRemove]; + return bid; + } + + it('should return true when necessary information is found', function () { + expect(spec.isBidRequestValid(mockVideoBid())).to.be.true; + }); + + it('should return false when necessary information is not found', function () { + // empty bidId + expect(spec.isBidRequestValid(mockVideoBid({bidId: ''}))).to.be.false; - // empty bidId - bid.bidId = ''; - expect(spec.isBidRequestValid(bid)).to.be.false; + // empty adUnitCode + expect(spec.isBidRequestValid(mockVideoBid({adUnitCode: ''}))).to.be.false; + }); + + it('should return false when required mediaTypes.video.* param is not found', function () { + const getBidAndExclude = paramToRemove => getVideoBidWithoutParam('mediaTypes.video', paramToRemove); + + expect(spec.isBidRequestValid(getBidAndExclude('playerSize'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('mimes'))).to.be.false; + }); + + it('should return false when required bid.params.* is not found', function () { + const getBidAndExclude = paramToRemove => getVideoBidWithoutParam('params', paramToRemove); + + expect(spec.isBidRequestValid(getBidAndExclude('placementId'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('video'))).to.be.false; + }); - // empty adUnitCode - bid.bidId = '30b31c1838de1e'; - bid.adUnitCode = ''; - expect(spec.isBidRequestValid(bid)).to.be.false; + it('should return false when required bid.params.video.* is not found', function () { + const getBidAndExclude = paramToRemove => getVideoBidWithoutParam('params.video', paramToRemove); - bid.adUnitCode = 'adunit-code'; + expect(spec.isBidRequestValid(getBidAndExclude('placement'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('maxduration'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('startdelay'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('protocols'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('api'))).to.be.false; + }); }); }); describe('buildRequests', function () { - it('should attempt to send bid requests to the endpoint via GET', function () { - const request = spec.buildRequests(bidArray, bidderRequest); - expect(request.method).to.equal('GET'); - expect(request.url).to.be.equal(ENDPOINT); - }); + const build = (bidRequests, bidderReq = mockBidderRequest()) => spec.buildRequests(bidRequests, bidderReq); + const buildAndGetPlacementInfo = (bidRequests, index = 0, bidderReq = mockBidderRequest()) => + utils.deepAccess(build(bidRequests, bidderReq), `${index}.data.p`); + const buildAndGetData = (bidRequests, index = 0, bidderReq = mockBidderRequest()) => + utils.deepAccess(build(bidRequests, bidderReq), `${index}.data`) || {}; - it('should not blow up if crumbs is undefined', function () { - let bidArray = [{ ...bid, crumbs: undefined }]; - expect(function () { - spec.buildRequests(bidArray, bidderRequest); - }).not.to.throw(); - }); + describe('Banner:', function () { + it('should attempt to send banner bid requests to the endpoint via GET', function () { + const requests = build([mockBannerBid()]); + expect(requests.length).to.equal(1); + expect(requests[0].method).to.equal('GET'); + expect(requests[0].url).to.be.equal(BANNER_ENDPOINT); + }); - it('should place bid information into the p parameter of data', function () { - let placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.equal( - '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1}]' - ); - bidArray.push({ - bidder: 'yieldmo', - params: { - bidFloor: 0.2, - }, - adUnitCode: 'adunit-code-1', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '123456789', - bidderRequestId: '987654321', - auctionId: '0246810', - crumbs: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da', - }, + it('should not blow up if crumbs is undefined', function () { + expect(function () { + build([mockBannerBid({crumbs: undefined})]); + }).not.to.throw(); }); - // multiple placements - placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.equal( - '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1},{"placement_id":"adunit-code-1","callback_id":"123456789","sizes":[[300,250],[300,600]],"bidFloor":0.2}]' - ); - }); + it('should place bid information into the p parameter of data', function () { + let bidArray = [mockBannerBid()]; + expect(buildAndGetPlacementInfo(bidArray)).to.equal( + '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1}]' + ); - it('should add placement id if given', function () { - bidArray[0].params.placementId = 'ym_1293871298'; - let placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); - expect(placementInfo).not.to.include('"ym_placement_id":"ym_0987654321"'); + // multiple placements + bidArray.push(mockBannerBid( + {adUnitCode: 'adunit-2', bidId: '123a', bidderRequestId: '321', auctionId: '222'}, {bidFloor: 0.2})); + expect(buildAndGetPlacementInfo(bidArray)).to.equal( + '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1},' + + '{"placement_id":"adunit-2","callback_id":"123a","sizes":[[300,250],[300,600]],"bidFloor":0.2}]' + ); + }); - bidArray[1].params.placementId = 'ym_0987654321'; - placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); - expect(placementInfo).to.include('"ym_placement_id":"ym_0987654321"'); - }); + it('should add placement id if given', function () { + let bidArray = [mockBannerBid({}, {placementId: 'ym_1293871298'})]; + let placementInfo = buildAndGetPlacementInfo(bidArray); + expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); + expect(placementInfo).not.to.include('"ym_placement_id":"ym_0987654321"'); - it('should add additional information to data parameter of request', function () { - const data = spec.buildRequests(bidArray, bidderRequest).data; - expect(data.hasOwnProperty('page_url')).to.be.true; - expect(data.hasOwnProperty('bust')).to.be.true; - expect(data.hasOwnProperty('pr')).to.be.true; - expect(data.hasOwnProperty('scrd')).to.be.true; - expect(data.dnt).to.be.false; - expect(data.hasOwnProperty('description')).to.be.true; - expect(data.hasOwnProperty('title')).to.be.true; - expect(data.hasOwnProperty('h')).to.be.true; - expect(data.hasOwnProperty('w')).to.be.true; - expect(data.hasOwnProperty('pubcid')).to.be.true; - expect(data.userConsent).to.equal('{"gdprApplies":"","cmp":""}'); - expect(data.us_privacy).to.equal(''); - }); + bidArray.push(mockBannerBid({}, {placementId: 'ym_0987654321'})); + placementInfo = buildAndGetPlacementInfo(bidArray); + expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); + expect(placementInfo).to.include('"ym_placement_id":"ym_0987654321"'); + }); - it('should add pubcid as parameter of request', function () { - const pubcidBid = { - bidder: 'yieldmo', - params: {}, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - userId: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2', - }, - }; - const data = spec.buildRequests([pubcidBid], bidderRequest).data; - expect(data.pubcid).to.deep.equal( - 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2' - ); - }); + it('should add additional information to data parameter of request', function () { + const data = buildAndGetData([mockBannerBid()]); + expect(data.hasOwnProperty('page_url')).to.be.true; + expect(data.hasOwnProperty('bust')).to.be.true; + expect(data.hasOwnProperty('pr')).to.be.true; + expect(data.hasOwnProperty('scrd')).to.be.true; + expect(data.dnt).to.be.false; + expect(data.hasOwnProperty('description')).to.be.true; + expect(data.hasOwnProperty('title')).to.be.true; + expect(data.hasOwnProperty('h')).to.be.true; + expect(data.hasOwnProperty('w')).to.be.true; + expect(data.hasOwnProperty('pubcid')).to.be.true; + expect(data.userConsent).to.equal('{"gdprApplies":"","cmp":""}'); + expect(data.us_privacy).to.equal(''); + }); - it('should add unified id as parameter of request', function () { - const unifiedIdBid = { - bidder: 'yieldmo', - params: {}, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - userId: { - tdid, - }, - }; - const data = spec.buildRequests([unifiedIdBid], bidderRequest).data; - expect(data.tdid).to.deep.equal(tdid); - }); + it('should add pubcid as parameter of request', function () { + const pubcid = 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2'; + const pubcidBid = mockBannerBid({crumbs: undefined, userId: {pubcid}}); + expect(buildAndGetData([pubcidBid]).pubcid).to.deep.equal(pubcid); + }); - it('should add CRITEO RTUS id as parameter of request', function () { - const criteoIdBid = { - bidder: 'yieldmo', - params: {}, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - userId: { - criteoId, - }, - }; - const data = spec.buildRequests([criteoIdBid], bidderRequest).data; - expect(data.cri_prebid).to.deep.equal(criteoId); - }); + it('should add unified id as parameter of request', function () { + const unifiedIdBid = mockBannerBid({crumbs: undefined}); + expect(buildAndGetData([unifiedIdBid]).tdid).to.deep.equal(mockBannerBid().userId.tdid); + }); + + it('should add CRITEO RTUS id as parameter of request', function () { + const criteoId = 'aff4'; + const criteoIdBid = mockBannerBid({crumbs: undefined, userId: { criteoId }}); + expect(buildAndGetData([criteoIdBid]).cri_prebid).to.deep.equal(criteoId); + }); - it('should add gdpr information to request if available', () => { - bidderRequest.gdprConsent = { - consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', - vendorData: { blerp: 1 }, - gdprApplies: true, - }; - const data = spec.buildRequests(bidArray, bidderRequest).data; - expect(data.userConsent).equal( - JSON.stringify({ + it('should add gdpr information to request if available', () => { + const gdprConsent = { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: {blerp: 1}, gdprApplies: true, - cmp: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', - }) - ); - }); + }; + const data = buildAndGetData([mockBannerBid()], 0, mockBidderRequest({gdprConsent})); + expect(data.userConsent).equal( + JSON.stringify({ + gdprApplies: true, + cmp: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + }) + ); + }); + + it('should add ccpa information to request if available', () => { + const uspConsent = '1YNY'; + const data = buildAndGetData([mockBannerBid()], 0, mockBidderRequest({uspConsent})); + expect(data.us_privacy).equal(uspConsent); + }); - it('should add ccpa information to request if available', () => { - const privacy = '1YNY'; - bidderRequest.uspConsent = privacy; - const data = spec.buildRequests(bidArray, bidderRequest).data; - expect(data.us_privacy).equal(privacy); + it('should add schain if it is in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{asi: 'indirectseller.com', sid: '00001', hp: 1}], + }; + const data = buildAndGetData([mockBannerBid({schain})]); + expect(data.schain).equal(JSON.stringify(schain)); + }); }); - it('should add schain if it is in the bidRequest', () => { - const schain = { - ver: '1.0', - complete: 1, - nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], - }; - bidArray[0].schain = schain; - const request = spec.buildRequests([bidArray[0]], bidderRequest); - expect(request.data.schain).equal(JSON.stringify(schain)); + describe('Instream video:', function () { + it('should attempt to send banner bid requests to the endpoint via POST', function () { + const requests = build([mockVideoBid()]); + expect(requests.length).to.equal(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.be.equal(VIDEO_ENDPOINT); + }); }); }); describe('interpretResponse', function () { - let serverResponse; - - beforeEach(function () { - serverResponse = { - body: [ - { - callback_id: '21989fdbef550a', - cpm: 3.45455, - width: 300, - height: 250, - ad: - '
', - creative_id: '9874652394875', - }, - ], - header: 'header?', - }; + const mockServerResponse = () => ({ + body: [{ + callback_id: '21989fdbef550a', + cpm: 3.45455, + width: 300, + height: 250, + ad: '' + + '
', + creative_id: '9874652394875', + }], + header: 'header?', }); it('should correctly reorder the server response', function () { - const newResponse = spec.interpretResponse(serverResponse); + const newResponse = spec.interpretResponse(mockServerResponse()); expect(newResponse.length).to.be.equal(1); expect(newResponse[0]).to.deep.equal({ requestId: '21989fdbef550a', @@ -289,19 +294,18 @@ describe('YieldmoAdapter', function () { currency: 'USD', netRevenue: true, ttl: 300, - ad: - '
', + ad: '' + + '
', }); }); it('should not add responses if the cpm is 0 or null', function () { - serverResponse.body[0].cpm = 0; - let response = spec.interpretResponse(serverResponse); - expect(response).to.deep.equal([]); + let response = mockServerResponse(); + response.body[0].cpm = 0; + expect(spec.interpretResponse(response)).to.deep.equal([]); - serverResponse.body[0].cpm = null; - response = spec.interpretResponse(serverResponse); - expect(response).to.deep.equal([]); + response.body[0].cpm = null; + expect(spec.interpretResponse(response)).to.deep.equal([]); }); }); diff --git a/test/spec/modules/zetaBidAdapter_spec.js b/test/spec/modules/zetaBidAdapter_spec.js new file mode 100644 index 00000000000..0d11614c926 --- /dev/null +++ b/test/spec/modules/zetaBidAdapter_spec.js @@ -0,0 +1,79 @@ +import { spec } from '../../../modules/zetaBidAdapter.js' + +describe('Zeta Bid Adapter', function() { + const bannerRequest = [{ + bidId: 12345, + auctionId: 67890, + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + refererInfo: { + referer: 'zetaglobal.com' + }, + params: { + placement: 12345, + user: { + uid: 12345, + buyeruid: 12345 + }, + ip: '111.222.33.44', + definerId: 1, + test: 1 + } + }]; + + it('Test the bid validation function', function() { + const validBid = spec.isBidRequestValid(bannerRequest[0]); + const invalidBid = spec.isBidRequestValid(null); + + expect(validBid).to.be.true; + expect(invalidBid).to.be.false; + }); + + it('Test the request processing function', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + expect(request).to.not.be.empty; + + const payload = request.data; + expect(payload).to.not.be.empty; + }); + + const responseBody = { + id: '12345', + seatbid: [ + { + bid: [ + { + id: 'auctionId', + impid: 'impId', + price: 0.0, + adm: 'adMarkup', + crid: 'creativeId', + h: 250, + w: 300 + } + ] + } + ], + cur: 'USD' + }; + + it('Test the response parsing function', function () { + const receivedBid = responseBody.seatbid[0].bid[0]; + const response = {}; + response.body = responseBody; + + const bidResponse = spec.interpretResponse(response, null); + expect(bidResponse).to.not.be.empty; + + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.ad).to.equal(receivedBid.adm); + expect(bid.cpm).to.equal(receivedBid.price); + expect(bid.height).to.equal(receivedBid.h); + expect(bid.width).to.equal(receivedBid.w); + expect(bid.requestId).to.equal(receivedBid.impid); + }); +}); diff --git a/test/spec/renderer_spec.js b/test/spec/renderer_spec.js index 9bf551f35e8..dcca94396cd 100644 --- a/test/spec/renderer_spec.js +++ b/test/spec/renderer_spec.js @@ -155,6 +155,36 @@ describe('Renderer', function () { expect(loadExternalScript.called).to.be.true; }); + it('should load external script instead of publisher-defined one when backupOnly option is true in mediaTypes.video options', function() { + $$PREBID_GLOBAL$$.adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'outstream', + mimes: ['video/mp4'], + playerSize: [[400, 300]], + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + backupOnly: true, + render: sinon.spy() + }, + } + } + }] + + let testRenderer = Renderer.install({ + url: 'https://httpbin.org/post', + config: { test: 'config1' }, + id: 1, + adUnitCode: 'video1' + + }); + testRenderer.setRender(() => {}) + + testRenderer.render() + expect(loadExternalScript.called).to.be.true; + }); + it('should call loadExternalScript() for script not defined on adUnit, only when .render() is called', function() { $$PREBID_GLOBAL$$.adUnits = [{ code: 'video1', diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 72a585049c3..3ce8ba081da 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -71,6 +71,29 @@ describe('video.js', function () { expect(valid).to.equal(true); }); + it('validates valid outstream bids with a publisher defined renderer', function () { + const bid = { + requestId: '123abc', + }; + const bidRequests = [{ + bids: [{ + bidId: '123abc', + bidder: 'appnexus', + mediaTypes: { + video: { + context: 'outstream', + renderer: { + url: 'render.url', + render: () => true, + } + } + } + }] + }]; + const valid = isValidVideoBid(bid, bidRequests); + expect(valid).to.equal(true); + }); + it('catches invalid outstream bids', function () { const bid = { requestId: '123abc'