diff --git a/PR_REVIEW.md b/PR_REVIEW.md index d7703cf20ae..a8b68c7ab45 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -6,38 +6,28 @@ If the PR is for a standard bid adapter or a standard analytics adapter, just th For modules and core platform updates, the initial reviewer should request an additional team member to review as a sanity check. Merge should only happen when the PR has 2 `LGTM` from the core team and a documentation PR if required. ### General PR review Process +- All required global and bidder-adapter rules defined in the [Module Rules](https://docs.prebid.org/dev-docs/module-rules.html) must be followed. Please review these rules often - we depend on reviewers to enforce them. - Checkout the branch (these instructions are available on the github PR page as well). - Verify PR is a single change type. Example, refactor OR bugfix. If more than 1 type, ask submitter to break out requests. -- Verify code under review has at least 80% unit test coverage. If legacy code has no unit test coverage, ask for unit tests to be included in the PR. +- Verify code under review has at least 80% unit test coverage. If legacy code doesn't have enough unit test coverage, require that additional unit tests to be included in the PR. - Verify tests are green in Travis-ci + local build by running `gulp serve` | `gulp test` - Verify no code quality violations are present from linting (should be reported in terminal) +- Make sure the code is not setting cookies or localstorage directly -- it must use the `StorageManager`. - Review for obvious errors or bad coding practice / use best judgement here. - If the change is a new feature / change to core prebid.js - review the change with a Tech Lead on the project and make sure they agree with the nature of change. - If the change results in needing updates to docs (such as public API change, module interface etc), add a label for "needs docs" and inform the submitter they must submit a docs PR to update the appropriate area of Prebid.org **before the PR can merge**. Help them with finding where the docs are located on prebid.org if needed. - - Below are some examples of bidder specific updates that should require docs update (in their dev-docs/bidders/BIDDER.md file): - - If they support the GDPR consentManagement module and TCF1, add `gdpr_supported: true` - - If they support the GDPR consentManagement module and TCF2, add `tcf2_supported: true` - - If they support the US Privacy consentManagementUsp module, add `usp_supported: true` - - If they support one or more userId modules, add `userId: (list of supported vendors)` - - If they support video and/or native mediaTypes add `media_types: video, native`. Note that display is added by default. If you don't support display, add "no-display" as the first entry, e.g. `media_types: no-display, native` - - If they support COPPA, add `coppa_supported: true` - - If they support SChain, add `schain_supported: true` - - If their bidder doesn't work well with safeframed creatives, add `safeframes_ok: false`. This will alert publishers to not use safeframed creatives when creating the ad server entries for their bidder. - - If they're setting a deal ID in some scenarios, add `bidder_supports_deals: true` - - If they have an IAB Global Vendor List ID, add `gvl_id: ID`. There's no default. -- If all above is good, add a `LGTM` comment and request 1 additional core member to review. -- Once there is 2 `LGTM` on the PR, merge to master -- Ask the submitter to add a PR for documentation if applicable. +- If all above is good, add a `LGTM` comment and, if the change is in PBS-core or is an important module like the prebidServerBidAdapter, request 1 additional core member to review. +- Once there are 2 `LGTM` on the PR, merge to master - Add a line into the [draft release](https://github.com/prebid/Prebid.js/releases) notes for this submission. If no draft release is available, create one using [this template]( https://gist.github.com/mkendall07/c3af6f4691bed8a46738b3675cb5a479) -- Add the PR to the appropriate project board (I.E. 1.23.0 Release) for the week, [see](https://github.com/prebid/Prebid.js/projects) -### New Adapter or updates to adapter process -- Follow steps above for general review process. In addition, please verify the following: +### Reviewing a New or Updated Bid Adapter +Documentation they're supposed to be following is https://docs.prebid.org/dev-docs/bidder-adaptor.html + +Follow steps above for general review process. In addition, please verify the following: - Verify that bidder has submitted valid bid params and that bids are being received. - Verify that bidder is not manipulating the prebid.js auction in any way or doing things that go against the principles of the project. If unsure check with the Tech Lead. - Verify that code re-use is being done properly and that changes introduced by a bidder don't impact other bidders. - If the adapter being submitted is an alias type, check with the bidder contact that is being aliased to make sure it's allowed. -- All required global and bidder-adapter rules defined in the [Module Rules](https://docs.prebid.org/dev-docs/module-rules.html) must be followed. Please review these rules often - we depend on reviewers to enforce them. - All bidder parameter conventions must be followed: - Video params must be read from AdUnit.mediaTypes.video when available; however bidder config can override the ad unit. - First party data must be read from [`fpd.context` and `fpd.user`](https://docs.prebid.org/dev-docs/publisher-api-reference.html#setConfig-fpd). @@ -45,9 +35,64 @@ For modules and core platform updates, the initial reviewer should request an ad - 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'); - +- Below are some examples of bidder specific updates that should require docs update (in their dev-docs/bidders/BIDDER.md file): + - If they support the GDPR consentManagement module and TCF1, add `gdpr_supported: true` + - If they support the GDPR consentManagement module and TCF2, add `tcf2_supported: true` + - If they support the US Privacy consentManagementUsp module, add `usp_supported: true` + - If they support one or more userId modules, add `userId: (list of supported vendors)` + - If they support video and/or native mediaTypes add `media_types: video, native`. Note that display is added by default. If you don't support display, add "no-display" as the first entry, e.g. `media_types: no-display, native` + - If they support COPPA, add `coppa_supported: true` + - If they support SChain, add `schain_supported: true` + - If their bidder doesn't work well with safeframed creatives, add `safeframes_ok: false`. This will alert publishers to not use safeframed creatives when creating the ad server entries for their bidder. + - If they're setting a deal ID in some scenarios, add `bidder_supports_deals: true` + - If they have an IAB Global Vendor List ID, add `gvl_id: ID`. There's no default. - After a new adapter is approved, let the submitter know they may open a PR in the [headerbid-expert repository](https://github.com/prebid/headerbid-expert) to have their adapter recognized by the [Headerbid Expert extension](https://chrome.google.com/webstore/detail/headerbid-expert/cgfkddgbnfplidghapbbnngaogeldmop). The PR should be to the [bidder patterns file](https://github.com/prebid/headerbid-expert/blob/master/bidderPatterns.js), adding an entry with their adapter's name and the url the adapter uses to send and receive bid responses. +### Reviewing a New or Updated Analytics Adapter +Documentation they're supposed to be following is https://docs.prebid.org/dev-docs/integrate-with-the-prebid-analytics-api.html + +No additional steps above the general review process and making sure it conforms to the [Module Rules](https://docs.prebid.org/dev-docs/module-rules.html). + +Make sure there's a docs pull request + +### Reviewing a New or Updated User ID Sub-Module +Documentation they're supposed to be following is https://docs.prebid.org/dev-docs/modules/userId.html#id-providers + +Follow steps above for general review process. In addition: +- Try running the new user ID module with a basic config and confirm it hits the endpoint and stores the results. +- the filename should be camel case ending with `IdSystem` (e.g. `myCompanyIdSystem.js`) +- the `const MODULE_NAME` value should be camel case ending with `Id` (e.g. `myCompanyId` ) +- the response of the `decode` method should be an object with the key being ideally camel case similar to the module name and ending in `id` or `Id`, but in some cases this value is a shortened name and sometimes with the `id` part being all lowercase, provided there are no other uppercase letters. if there's no id or it's an invalid object, the response should be `undefined`. example "valid" values (although this is more style than a requirement) + - `mcid` + - `mcId` + - `myCompanyId` +- make sure they've added references of their new module everywhere required: + - modules/.submodules.json + - modules/userId/eids.js + - modules/userId/eids.md + - modules/userId/userId.md +- tests can go either within the userId_spec.js file or in their own _spec file if they wish +- GVLID is recommended in the *IdSystem file if they operate in EU +- make sure example configurations align to the actual code (some modules use the userId storage settings and allow pub configuration, while others handle reading/writing cookies on their own, so should not include the storage params in examples) +- the 3 available methods (getId, extendId, decode) should be used as they were intended + - decode (required method) should not be making requests to retrieve a new ID, it should just be decoding a response + - extendId (optional method) should not be making requests to retrieve a new ID, it should just be adding additional data to the id object + - getId (required method) should be the only method that gets a new ID (from ajax calls or a cookie/local storage). this ensures that decode and extend do not unnecessarily delay the auction in places where it is not expected. +- in the eids.js file, the source should be the actual domain of the provider, not a made up domain. +- in the eids.js file, the key in the array should be the same value as the key in the decode function +- make sure all supported config params align in the submodule js file and the docs / examples +- make sure there's a docs pull request + +### Reviewing a New or Updated Real-Time-Data Sub-Module +Documentation they're supposed to be following is https://docs.prebid.org/dev-docs/add-rtd-submodule.html + +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. +- 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 + ## Ticket Coordinator Each week, Prebid Org assigns one person to keep an eye on incoming issues and PRs. Every Monday morning a reminder is diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index dddc0915db2..7375293fdf0 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -269,7 +269,10 @@ }, { name: "quantcastId" - } + }, + { + name: "criteo" + }, ], syncDelay: 5000, auctionDelay: 1000 diff --git a/modules/.submodules.json b/modules/.submodules.json index 18e75dd1794..91cda9d95ad 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -15,7 +15,9 @@ "intentIqIdSystem", "zeotapIdPlusIdSystem", "haloIdSystem", - "quantcastIdSystem" + "quantcastIdSystem", + "idxIdSystem", + "fabrickIdSystem" ], "adpod": [ "freeWheelAdserverVideo", diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index e22aafb73fc..51138a2cac7 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -6,12 +6,23 @@ import { Renderer } from '../src/Renderer.js'; import find from 'core-js-pure/features/array/find.js'; const subdomainSuffixes = ['', 1, 2]; -const getUri = (function () { - let num = 0; - return function () { - return 'https://ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com/v2/auction/' +const AUCTION_PATH = '/v2/auction/'; +const PROTOCOL = 'https://'; +const HOST_GETTERS = { + default: (function () { + let num = 0; + return function () { + return 'ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com' + } + }()), + appaloosa: function () { + return 'hb.appaloosa.media' } -}()) +} +const getUri = function (bidderCode) { + let getter = HOST_GETTERS[bidderCode] || HOST_GETTERS['default']; + return PROTOCOL + getter() + AUCTION_PATH +} const OUTSTREAM_SRC = 'https://player.adtelligent.com/outstream-unit/2.01/outstream.min.js'; const BIDDER_CODE = 'adtelligent'; const OUTSTREAM = 'outstream'; @@ -21,7 +32,7 @@ const syncsCache = {}; export const spec = { code: BIDDER_CODE, gvlid: 410, - aliases: ['onefiftytwomedia', 'selectmedia'], + aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { return !!utils.deepAccess(bid, 'params.aid'); @@ -82,7 +93,7 @@ export const spec = { data: Object.assign({}, tag, { BidRequests: bids }), adapterRequest, method: 'POST', - url: getUri() + url: getUri(adapterRequest.bidderCode) }; }) }, diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 725ee0f5626..40d3cf84369 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,5 +1,6 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; import find from 'core-js-pure/features/array/find.js'; const VERSION = '1.0'; @@ -104,15 +105,6 @@ function getHostname(bidderRequest) { return ''; } -/* Get current page referrer url */ -function getReferrerUrl(bidderRequest) { - let referer = ''; - if (bidderRequest && bidderRequest.refererInfo) { - referer = bidderRequest.refererInfo.referer; - } - return referer; -} - /* Get current page canonical url */ function getCanonicalUrl() { let link; @@ -155,9 +147,14 @@ function createEndpoint(bidRequests, bidderRequest) { function createEndpointQS(bidderRequest) { const qs = {}; - const ref = getReferrerUrl(bidderRequest); - if (ref) { - qs.RefererUrl = encodeURIComponent(ref); + if (bidderRequest) { + const ref = bidderRequest.refererInfo; + if (ref) { + qs.RefererUrl = encodeURIComponent(ref.referer); + if (ref.numIframes > 0) { + qs.SafeFrame = true; + } + } } const can = getCanonicalUrl(); @@ -165,6 +162,11 @@ function createEndpointQS(bidderRequest) { qs.CanonicalUrl = encodeURIComponent(can); } + const domain = config.getConfig('publisherDomain'); + if (domain) { + qs.PublisherDomain = encodeURIComponent(domain); + } + return qs; } diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index f3ebf9bf4f6..ff0e3230007 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -157,6 +157,7 @@ export const spec = { const memberIdBid = find(bidRequests, hasMemberId); const member = memberIdBid ? parseInt(memberIdBid.params.member, 10) : 0; const schain = bidRequests[0].schain; + const omidSupport = find(bidRequests, hasOmidSupport); const payload = { tags: [...tags], @@ -168,6 +169,13 @@ export const spec = { schain: schain }; + if (omidSupport) { + payload['iab_support'] = { + omidpn: 'Appnexus', + omidpv: '$prebid.version$' + } + } + if (member > 0) { payload.member_id = member; } @@ -766,16 +774,27 @@ function bidToTag(bid) { type = (utils.isArray(type)) ? type[0] : type; tag.video[param] = VIDEO_MAPPING[param][type]; break; + // Deprecating tags[].video.frameworks in favor of tags[].video_frameworks + case 'frameworks': + break; default: tag.video[param] = bid.params.video[param]; } }); + + if (bid.params.video.frameworks && utils.isArray(bid.params.video.frameworks)) { + tag['video_frameworks'] = bid.params.video.frameworks; + } } if (bid.renderer) { tag.video = Object.assign({}, tag.video, { custom_renderer_present: true }); } + if (bid.params.frameworks && utils.isArray(bid.params.frameworks)) { + tag['banner_frameworks'] = bid.params.frameworks; + } + let adUnit = find(auctionManager.getAdUnits(), au => bid.transactionId === au.transactionId); if (adUnit && adUnit.mediaTypes && adUnit.mediaTypes.banner) { tag.ad_types.push(BANNER); @@ -844,6 +863,19 @@ function hasAdPod(bid) { ); } +function hasOmidSupport(bid) { + let hasOmid = false; + const bidderParams = bid.params; + const videoParams = bid.params.video; + if (bidderParams.frameworks && utils.isArray(bidderParams.frameworks)) { + hasOmid = includes(bid.params.frameworks, 6); + } + if (!hasOmid && videoParams && videoParams.frameworks && utils.isArray(videoParams.frameworks)) { + hasOmid = includes(bid.params.video.frameworks, 6); + } + return hasOmid; +} + /** * Expand an adpod placement into a set of request objects according to the * total adpod duration and the range of duration seconds. Sets minduration/ diff --git a/modules/atsAnalyticsAdapter.js b/modules/atsAnalyticsAdapter.js index 9811c306738..a5bb3797bbf 100644 --- a/modules/atsAnalyticsAdapter.js +++ b/modules/atsAnalyticsAdapter.js @@ -126,13 +126,16 @@ let atsAnalyticsAdapter = Object.assign(adapter( callHandler(eventType, args); } if (eventType === CONSTANTS.EVENTS.AUCTION_END) { - // send data to ats analytic endpoint - try { - let dataToSend = {'Data': atsAnalyticsAdapter.context.events}; - let strJSON = JSON.stringify(dataToSend); - ajax(atsAnalyticsAdapter.context.host, function () { - }, strJSON, {method: 'POST', contentType: 'application/json'}); - } catch (err) { + if (atsAnalyticsAdapter.shouldFireRequest()) { + // send data to ats analytic endpoint + try { + let dataToSend = {'Data': atsAnalyticsAdapter.context.events}; + let strJSON = JSON.stringify(dataToSend); + utils.logInfo('atsAnalytics tried to send analytics data!'); + ajax(atsAnalyticsAdapter.context.host, function () { + }, strJSON, {method: 'POST', contentType: 'application/json'}); + } catch (err) { + } } } } @@ -141,6 +144,11 @@ let atsAnalyticsAdapter = Object.assign(adapter( // save the base class function atsAnalyticsAdapter.originEnableAnalytics = atsAnalyticsAdapter.enableAnalytics; +// add check to not fire request every time, but instead to send 1/10 events +atsAnalyticsAdapter.shouldFireRequest = function () { + return (Math.floor((Math.random() * 11)) === 10); +} + // override enableAnalytics so we can get access to the config passed in from the page atsAnalyticsAdapter.enableAnalytics = function (config) { if (!config.options.pid) { diff --git a/modules/britepoolIdSystem.js b/modules/britepoolIdSystem.js index 90fd159571f..3bf416957d2 100644 --- a/modules/britepoolIdSystem.js +++ b/modules/britepoolIdSystem.js @@ -29,11 +29,12 @@ export const britepoolIdSubmodule = { /** * Performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [submoduleConfigParams] + * @param {SubmoduleConfig} [submoduleConfig] * @param {ConsentData|undefined} consentData * @returns {function(callback:function)} */ - getId(submoduleConfigParams, consentData) { + getId(submoduleConfig, consentData) { + const submoduleConfigParams = (submoduleConfig && submoduleConfig.params) || {}; const { params, headers, url, getter, errors } = britepoolIdSubmodule.createParams(submoduleConfigParams, consentData); let getterResponse = null; if (typeof getter === 'function') { diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index 017194d0e86..83bc773cb30 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -31,7 +31,7 @@ function areCookiesWriteable() { } function extractProtocolHost (url, returnOnlyHost = false) { - const parsedUrl = utils.parseUrl(url) + const parsedUrl = utils.parseUrl(url, {noDecodeWholeURL: true}) return returnOnlyHost ? `${parsedUrl.hostname}` : `${parsedUrl.protocol}://${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}/`; @@ -103,7 +103,9 @@ function callCriteoUserSync(parsedCriteoData, gdprString) { } else if (jsonResponse.bundle) { saveOnAllStorages(bundleStorageKey, jsonResponse.bundle); } - } + }, + undefined, + { method: 'GET', contentType: 'application/json', withCredentials: true } ); } @@ -126,11 +128,11 @@ export const criteoIdSubmodule = { /** * get the Criteo Id from local storages and initiate a new user sync * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @param {ConsentData} [consentData] * @returns {{id: {criteoId: string} | undefined}}} */ - getId(configParams, consentData) { + getId(config, consentData) { const hasGdprData = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies; const gdprConsentString = hasGdprData ? consentData.consentString : undefined; diff --git a/modules/fabrickIdSystem.js b/modules/fabrickIdSystem.js new file mode 100644 index 00000000000..e61b377eefa --- /dev/null +++ b/modules/fabrickIdSystem.js @@ -0,0 +1,147 @@ +/** + * This module adds neustar's fabrickId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/fabrickIdSystem + * @requires module:modules/userId + */ + +import * as utils from '../src/utils.js' +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getRefererInfo } from '../src/refererDetection.js'; + +/** @type {Submodule} */ +export const fabrickIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: 'fabrickId', + + /** + * decode the stored id value for passing to bid requests + * @function decode + * @param {(Object|string)} value + * @returns {(Object|undefined)} + */ + decode(value) { + if (value && value.fabrickId) { + return { 'fabrickId': value.fabrickId }; + } else { + return undefined; + } + }, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function getId + * @param {SubmoduleConfig} [config] + * @param {ConsentData} + * @param {Object} cacheIdObj - existing id, if any consentData] + * @returns {IdResponse|undefined} + */ + getId(config, consentData, cacheIdObj) { + try { + const configParams = (config && config.params) || {}; + if (window.fabrickMod1) { + window.fabrickMod1(configParams, consentData, cacheIdObj); + } + if (!configParams || typeof configParams.apiKey !== 'string') { + utils.logError('fabrick submodule requires an apiKey.'); + return; + } + try { + let url = _getBaseUrl(configParams); + let keysArr = Object.keys(configParams); + for (let i in keysArr) { + let k = keysArr[i]; + if (k === 'url' || k === 'refererInfo') { + continue; + } + let v = configParams[k]; + if (Array.isArray(v)) { + for (let j in v) { + url += `${k}=${v[j]}&`; + } + } else { + url += `${k}=${v}&`; + } + } + // pull off the trailing & + url = url.slice(0, -1) + const referer = _getRefererInfo(configParams); + const urls = new Set(); + url = truncateAndAppend(urls, url, 'r', referer.referer); + if (referer.stack && referer.stack[0]) { + url = truncateAndAppend(urls, url, 'r', referer.stack[0]); + } + url = truncateAndAppend(urls, url, 'r', referer.canonicalUrl); + url = truncateAndAppend(urls, url, 'r', window.location.href); + + const resp = function (callback) { + const callbacks = { + success: response => { + if (window.fabrickMod2) { + return window.fabrickMod2( + callback, response, configParams, consentData, cacheIdObj); + } else { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + utils.logError(error); + responseObj = {}; + } + } + callback(responseObj); + } + }, + error: error => { + utils.logError(`fabrickId fetch encountered an error`, error); + callback(); + } + }; + ajax(url, callbacks, null, {method: 'GET', withCredentials: true}); + }; + return {callback: resp}; + } catch (e) { + utils.logError(`fabrickIdSystem encountered an error`, e); + } + } catch (e) { + utils.logError(`fabrickIdSystem encountered an error`, e); + } + } +}; + +function _getRefererInfo(configParams) { + if (configParams.refererInfo) { + return configParams.refererInfo; + } else { + return getRefererInfo(); + } +} + +function _getBaseUrl(configParams) { + if (configParams.url) { + return configParams.url; + } else { + return `https://fid.agkn.com/f?`; + } +} + +function truncateAndAppend(urls, url, paramName, s) { + if (s && url.length < 2000) { + if (s.length > 200) { + s = s.substring(0, 200); + } + // Don't send the same url in multiple params + if (!urls.has(s)) { + urls.add(s); + return `${url}&${paramName}=${s}` + } + } + return url; +} + +submodule('userId', fabrickIdSubmodule); diff --git a/modules/fabrickIdSystem.md b/modules/fabrickIdSystem.md new file mode 100644 index 00000000000..268c861710a --- /dev/null +++ b/modules/fabrickIdSystem.md @@ -0,0 +1,24 @@ +## Neustar Fabrick User ID Submodule + +Fabrick ID Module - https://www.home.neustar/fabrick +Product and Sales Inquiries: 1-855-898-0036 + +## Example configuration for publishers: +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'fabrickId', + storage: { + name: 'pbjs_fabrickId', + type: 'cookie', + expires: 7 + }, + params: { + apiKey: 'your apiKey', // provided to you by Neustar + e: '31c5543c1734d25c7206f5fd591525d0295bec6fe84ff82f946a34fe970a1e66' // example hash identifier (sha256) + } + }] + } +}); +``` diff --git a/modules/haloIdSystem.js b/modules/haloIdSystem.js index 237b502f6a7..d0eb79d4ac2 100644 --- a/modules/haloIdSystem.js +++ b/modules/haloIdSystem.js @@ -30,10 +30,10 @@ export const haloIdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId(configParams) { + getId(config) { const url = `https://id.halo.ad.gt/api/v1/pbhid`; const resp = function (callback) { diff --git a/modules/hybridBidAdapter.js b/modules/hybridBidAdapter.js index 5671756e0b2..dd55483ef33 100644 --- a/modules/hybridBidAdapter.js +++ b/modules/hybridBidAdapter.js @@ -10,12 +10,14 @@ const DSP_ENDPOINT = 'https://hbe198.hybrid.ai/prebidhb'; const TRAFFIC_TYPE_WEB = 1; const PLACEMENT_TYPE_BANNER = 1; const PLACEMENT_TYPE_VIDEO = 2; +const PLACEMENT_TYPE_IN_IMAGE = 3; const TTL = 60; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; const placementTypes = { 'banner': PLACEMENT_TYPE_BANNER, - 'video': PLACEMENT_TYPE_VIDEO + 'video': PLACEMENT_TYPE_VIDEO, + 'inImage': PLACEMENT_TYPE_IN_IMAGE }; function buildBidRequests(validBidRequests) { @@ -26,7 +28,8 @@ function buildBidRequests(validBidRequests) { transactionId: validBidRequest.transactionId, sizes: validBidRequest.sizes, placement: placementTypes[params.placement], - placeId: params.placeId + placeId: params.placeId, + imageUrl: params.imageUrl || '' }; return bidRequest; @@ -94,6 +97,33 @@ function buildBid(bidData) { bid.renderer = createRenderer(bid); } } + } else if (bidData.placement === PLACEMENT_TYPE_IN_IMAGE) { + bid.mediaType = BANNER; + bid.inImageContent = { + content: { + content: bidData.content, + actionUrls: {} + } + }; + let actionUrls = bid.inImageContent.content.actionUrls; + actionUrls.loadUrls = bidData.inImage.loadtrackers || []; + actionUrls.impressionUrls = bidData.inImage.imptrackers || []; + actionUrls.scrollActUrls = bidData.inImage.startvisibilitytrackers || []; + actionUrls.viewUrls = bidData.inImage.viewtrackers || []; + actionUrls.stopAnimationUrls = bidData.inImage.stopanimationtrackers || []; + actionUrls.closeBannerUrls = bidData.inImage.closebannertrackers || []; + + if (bidData.inImage.but) { + let inImageOptions = bid.inImageContent.content.inImageOptions = {}; + inImageOptions.hasButton = true; + inImageOptions.buttonLogoUrl = bidData.inImage.but_logo; + inImageOptions.buttonProductUrl = bidData.inImage.but_prod; + inImageOptions.buttonHead = bidData.inImage.but_head; + inImageOptions.buttonHeadColor = bidData.inImage.but_head_colour; + inImageOptions.dynparams = bidData.inImage.dynparams || {}; + } + + bid.ad = wrapAd(bid, bidData); } else { bid.ad = bidData.content; bid.mediaType = BANNER; @@ -116,6 +146,30 @@ function hasVideoMandatoryParams(mediaTypes) { return isHasVideoContext && isPlayerSize; } +function wrapAd(bid, bidData) { + return ` + + + + + + + + +
+ + + `; +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], @@ -133,6 +187,7 @@ export const spec = { !!bid.params.placement && ( (getMediaTypeFromBid(bid) === BANNER && bid.params.placement === 'banner') || + (getMediaTypeFromBid(bid) === BANNER && bid.params.placement === 'inImage' && !!bid.params.imageUrl) || (getMediaTypeFromBid(bid) === VIDEO && bid.params.placement === 'video' && hasVideoMandatoryParams(bid.mediaTypes)) ) ); diff --git a/modules/hybridBidAdapter.md b/modules/hybridBidAdapter.md index 245f010970a..098d8642415 100644 --- a/modules/hybridBidAdapter.md +++ b/modules/hybridBidAdapter.md @@ -52,3 +52,187 @@ var adUnits = [{ }]; ``` +# Sample In-Image Ad Unit + +```js +var adUnits = [{ + code: 'test-div', + mediaTypes: { + banner: { + sizes: [0, 0] + } + }, + bids: [{ + bidder: "hybrid", + params: { + placement: "inImage", + placeId: "102030405060708090000020", + imageUrl: "https://hybrid.ai/images/image.jpg" + } + }] +}]; +``` + +# Example page with In-Image + +```html + + + + + Prebid.js Banner Example + + + + + +

Prebid.js InImage Banner Test

+
+ + +
+ + +``` + +# Example page with In-Image and GPT + +```html + + + + + Prebid.js Banner Example + + + + + + +

Prebid.js Banner Ad Unit Test

+
+ + +
+ + +``` diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 47064e0a1a9..f8ff50f52a3 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -65,12 +65,13 @@ export const id5IdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function getId - * @param {SubmoduleParams} [configParams] - * @param {ConsentData} [consentData] + * @param {SubmoduleConfig} config + * @param {ConsentData} consentData * @param {(Object|undefined)} cacheIdObj * @returns {IdResponse|undefined} */ - getId(configParams, consentData, cacheIdObj) { + getId(config, consentData, cacheIdObj) { + const configParams = (config && config.params) || {}; if (!hasRequiredParams(configParams)) { return undefined; } @@ -123,11 +124,12 @@ export const id5IdSubmodule = { * If IdResponse#callback is defined, then it'll called at the end of auction. * It's permissible to return neither, one, or both fields. * @function extendId - * @param {SubmoduleParams} configParams + * @param {SubmoduleConfig} config * @param {Object} cacheIdObj - existing id, if any * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ - extendId(configParams, cacheIdObj) { + extendId(config, cacheIdObj) { + const configParams = (config && config.params) || {}; incrementNb(configParams); return cacheIdObj; } diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index 14c33329b2d..33dd74380ac 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -8,6 +8,9 @@ import * as utils from '../src/utils.js' import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; +import {getStorageManager} from '../src/storageManager.js'; + +export const storage = getStorageManager(); /** @type {Submodule} */ export const identityLinkSubmodule = { @@ -34,10 +37,11 @@ export const identityLinkSubmodule = { * performs action to obtain id and return a value in the callback's response argument * @function * @param {ConsentData} [consentData] - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId(configParams, consentData) { + getId(config, consentData) { + const configParams = (config && config.params) || {}; if (!configParams || typeof configParams.pid !== 'string') { utils.logError('identityLink submodule requires partner id to be defined'); return; @@ -75,7 +79,6 @@ export const identityLinkSubmodule = { }; // return envelope from third party endpoint function getEnvelope(url, callback) { - utils.logInfo('A 3P retrieval is attempted!'); const callbacks = { success: response => { let responseObj; @@ -93,7 +96,18 @@ function getEnvelope(url, callback) { callback(); } }; - ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); + + if (!storage.getCookie('_lr_retry_request')) { + setRetryCookie(); + utils.logInfo('A 3P retrieval is attempted!'); + ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); + } +} + +function setRetryCookie() { + let now = new Date(); + now.setTime(now.getTime() + 3600000); + storage.setCookie('_lr_retry_request', 'true', now.toUTCString()); } submodule('userId', identityLinkSubmodule); diff --git a/modules/idxIdSystem.js b/modules/idxIdSystem.js new file mode 100644 index 00000000000..00e8a8bc5e5 --- /dev/null +++ b/modules/idxIdSystem.js @@ -0,0 +1,61 @@ +/** + * This module adds IDx to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/idxIdSystem + * @requires module:modules/userId + */ +import * as utils from '../src/utils.js' +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const IDX_MODULE_NAME = 'idx'; +const IDX_COOKIE_NAME = '_idx'; +export const storage = getStorageManager(); + +function readIDxFromCookie() { + return storage.cookiesAreEnabled ? storage.getCookie(IDX_COOKIE_NAME) : null; +} + +function readIDxFromLocalStorage() { + return storage.localStorageIsEnabled ? storage.getDataFromLocalStorage(IDX_COOKIE_NAME) : null; +} + +/** @type {Submodule} */ +export const idxIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: IDX_MODULE_NAME, + /** + * decode the stored id value for passing to bid requests + * @function + * @param { Object | string | undefined } value + * @return { Object | string | undefined } + */ + decode(value) { + const idxVal = value ? utils.isStr(value) ? value : utils.isPlainObject(value) ? value.id : undefined : undefined; + return idxVal ? { + 'idx': idxVal + } : undefined; + }, + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} config + * @return {{id: string | undefined } | undefined} + */ + getId() { + const idxString = readIDxFromLocalStorage() || readIDxFromCookie(); + if (typeof idxString == 'string' && idxString) { + try { + const idxObj = JSON.parse(idxString); + return idxObj && idxObj.idx ? { id: idxObj.idx } : undefined; + } catch (error) { + utils.logError(error); + } + } + return undefined; + } +}; +submodule('userId', idxIdSubmodule); diff --git a/modules/idxIdSystem.md b/modules/idxIdSystem.md new file mode 100644 index 00000000000..363120899cb --- /dev/null +++ b/modules/idxIdSystem.md @@ -0,0 +1,22 @@ +## IDx User ID Submodule + +For assistance setting up your module please contact us at [prebid@idx.lat](prebid@idx.lat). + +### Prebid Params + +Individual params may be set for the IDx Submodule. +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'idx', + }] + } +}); +``` +## Parameter Descriptions for the `userSync` Configuration Section +The below parameters apply only to the IDx integration. + +| Param under usersync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID of the module - `"idx"` | `"idx"` | diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index bb74b1210cb..c22a6dbc7aa 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -56,10 +56,11 @@ export const intentIqIdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId(configParams) { + getId(config) { + const configParams = (config && config.params) || {}; if (!configParams || typeof configParams.partner !== 'number') { utils.logError('User ID - intentIqId submodule requires a valid partner to be defined'); return; diff --git a/modules/ironsourceBidAdapter.js b/modules/ironsourceBidAdapter.js index 34650d46a0f..922e7b94c5b 100644 --- a/modules/ironsourceBidAdapter.js +++ b/modules/ironsourceBidAdapter.js @@ -7,7 +7,11 @@ const SUPPORTED_AD_TYPES = [VIDEO]; const BIDDER_CODE = 'ironsource'; const BIDDER_VERSION = '4.0.0'; const TTL = 360; -const SELLER_ENDPOINT = 'https://hb.yellowblue.io/hb'; +const SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; +const MODES = { + PRODUCTION: 'hb', + TEST: 'hb-test' +} const SUPPORTED_SYNC_METHODS = { IFRAME: 'iframe', PIXEL: 'pixel' @@ -86,9 +90,10 @@ registerBidder(spec); */ function buildVideoRequest(bid, bidderRequest) { const sellerParams = generateParameters(bid, bidderRequest); + const {params} = bid; return { method: 'GET', - url: SELLER_ENDPOINT, + url: getEndpoint(params.testMode), data: sellerParams }; } @@ -169,6 +174,17 @@ function isSyncMethodAllowed(syncRule, bidderCode) { return isInclude && utils.contains(bidders, bidderCode); } +/** + * Get the seller endpoint + * @param testMode {boolean} + * @returns {string} + */ +function getEndpoint(testMode) { + return testMode + ? SELLER_ENDPOINT + MODES.TEST + : SELLER_ENDPOINT + MODES.PRODUCTION; +} + /** * Generate query parameters for the request * @param bid {bid} diff --git a/modules/ironsourceBidAdapter.md b/modules/ironsourceBidAdapter.md index 2f9e38b69e8..86756b08809 100644 --- a/modules/ironsourceBidAdapter.md +++ b/modules/ironsourceBidAdapter.md @@ -23,6 +23,7 @@ The adapter supports Video(instream). For the integration, IronSource returns co | `isOrg` | required | String | IronSource publisher Id provided by your IronSource representative | "56f91cd4d3e3660002000033" | `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 | `ifa` | optional | String | The ID for advertisers (also referred to as "IDFA") | "XXX-XXX" +| `testMode` | optional | Boolean | This activates the test mode | false # Test Parameters ```javascript @@ -42,6 +43,7 @@ var adUnits = [ isOrg: '56f91cd4d3e3660002000033', // Required floorPrice: 2.00, // Optional ifa: 'XXX-XXX', // Optional + testMode: false // Optional } }] } diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index bd2638e5936..7981b62dc51 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -100,10 +100,11 @@ export const liveIntentIdSubmodule = { * `publisherId` params. * @function * @param {{unifiedId:string}} value - * @param {SubmoduleParams|undefined} [configParams] + * @param {SubmoduleConfig|undefined} config * @returns {{lipb:Object}} */ - decode(value, configParams) { + decode(value, config) { + const configParams = (config && config.params) || {}; function composeIdObject(value) { const base = { 'lipbid': value['unifiedId'] }; delete value.unifiedId; @@ -121,10 +122,11 @@ export const liveIntentIdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId(configParams) { + getId(config) { + const configParams = (config && config.params) || {}; const liveConnect = initializeLiveConnect(configParams); if (!liveConnect) { return; diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index cdf9131dd68..5c3a9a16b3a 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -152,21 +152,22 @@ export const lotamePanoramaIdSubmodule = { * Decode the stored id value for passing to bid requests * @function decode * @param {(Object|string)} value + * @param {SubmoduleConfig|undefined} config * @returns {(Object|undefined)} */ - decode(value, configParams) { + decode(value, config) { return utils.isStr(value) ? { 'lotamePanoramaId': value } : undefined; }, /** * Retrieve the Lotame Panorama Id * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @param {ConsentData} [consentData] * @param {(Object|undefined)} cacheIdObj * @returns {IdResponse|undefined} */ - getId(configParams, consentData, cacheIdObj) { + getId(config, consentData, cacheIdObj) { let localCache = getLotameLocalCache(); let refreshNeeded = Date.now() > localCache.expiryTimestampMs; diff --git a/modules/mediaforceBidAdapter.js b/modules/mediaforceBidAdapter.js index 7e5c06b1b48..7e29bb519b5 100644 --- a/modules/mediaforceBidAdapter.js +++ b/modules/mediaforceBidAdapter.js @@ -114,18 +114,28 @@ export const spec = { } const referer = bidderRequest && bidderRequest.refererInfo ? encodeURIComponent(bidderRequest.refererInfo.referer) : ''; + const auctionId = bidderRequest && bidderRequest.auctionId; + const timeout = bidderRequest && bidderRequest.timeout; const dnt = utils.getDNT() ? 1 : 0; - let requests = []; + const requestsMap = {}; + const requests = []; + let isTest = false; validBidRequests.forEach(bid => { + isTest = isTest || bid.params.is_test; let tagid = bid.params.placement_id; let bidfloor = bid.params.bidfloor ? parseFloat(bid.params.bidfloor) : 0; - let imp = []; let validImp = false; let impObj = { id: bid.bidId, tagid: tagid, - secure: 1, + secure: window.location.protocol === 'https' ? 1 : 0, bidfloor: bidfloor, + ext: { + mediaforce: { + transactionId: bid.transactionId + } + } + }; for (let mediaTypes in bid.mediaTypes) { switch (mediaTypes) { @@ -140,31 +150,47 @@ export const spec = { default: return; } } - validImp && imp.push(impObj); - let request = { - id: bid.transactionId, - site: { - page: referer, - ref: referer, - id: bid.params.publisher_id, - publisher: { - id: bid.params.publisher_id + let request = requestsMap[bid.params.publisher_id]; + if (!request) { + request = { + id: Math.round(Math.random() * 1e16).toString(16), + site: { + page: window.location.href, + ref: referer, + id: bid.params.publisher_id, + publisher: { + id: bid.params.publisher_id + }, }, - }, - device: { - ua: navigator.userAgent, - js: 1, - dnt: dnt, - language: getLanguage() - }, - imp - }; - requests.push({ - method: 'POST', - url: bid.params.is_test ? TEST_ENDPOINT_URL : ENDPOINT_URL, - data: JSON.stringify(request) - }); + device: { + ua: navigator.userAgent, + js: 1, + dnt: dnt, + language: getLanguage() + }, + ext: { + mediaforce: { + hb_key: auctionId + } + }, + tmax: timeout, + imp: [] + }; + requestsMap[bid.params.publisher_id] = request; + requests.push({ + method: 'POST', + url: ENDPOINT_URL, + data: request + }); + } + validImp && request.imp.push(impObj); + }); + requests.forEach((req) => { + if (isTest) { + req.url = TEST_ENDPOINT_URL; + } + req.data = JSON.stringify(req.data); }); return requests; }, diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js index d6bf96618df..c55233af6a0 100644 --- a/modules/merkleIdSystem.js +++ b/modules/merkleIdSystem.js @@ -31,11 +31,12 @@ export const merkleIdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @param {ConsentData} [consentData] * @returns {IdResponse|undefined} */ - getId(configParams, consentData) { + getId(config, consentData) { + const configParams = (config && config.params) || {}; if (!configParams || typeof configParams.pubid !== 'string') { utils.logError('User ID - merkleId submodule requires a valid pubid to be defined'); return; diff --git a/modules/netIdSystem.js b/modules/netIdSystem.js index cfe78d9f488..90c8735c993 100644 --- a/modules/netIdSystem.js +++ b/modules/netIdSystem.js @@ -26,12 +26,12 @@ export const netIdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @param {ConsentData} [consentData] * @param {(Object|undefined)} cacheIdObj * @returns {IdResponse|undefined} */ - getId(configParams) { + getId(config) { /* currently not possible */ return {}; } diff --git a/modules/ooloAnalyticsAdapter.js b/modules/ooloAnalyticsAdapter.js new file mode 100644 index 00000000000..7195d6ac177 --- /dev/null +++ b/modules/ooloAnalyticsAdapter.js @@ -0,0 +1,547 @@ +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' +import { ajax } from '../src/ajax.js' +import { config } from '../src/config.js' + +const baseUrl = 'https://pbdata.oolo.io/' +const ENDPOINTS = { + CONFIG: 'https://config-pbdata.oolo.io', + PAGE_DATA: baseUrl + 'page', + AUCTION: baseUrl + 'auctionData', + BID_WON: baseUrl + 'bidWonData', + AD_RENDER_FAILED: baseUrl + 'adRenderFailedData', + RAW: baseUrl + 'raw', + HBCONFIG: baseUrl + 'hbconfig', +} + +const pbModuleVersion = '1.0.0' +const prebidVersion = '$prebid.version$' +const analyticsType = 'endpoint' +const ADAPTER_CODE = 'oolo' +const AUCTION_END_SEND_TIMEOUT = 1500 +export const PAGEVIEW_ID = +generatePageViewId() + +const { + AUCTION_INIT, + AUCTION_END, + BID_REQUESTED, + BID_RESPONSE, + NO_BID, + BID_WON, + BID_TIMEOUT, + AD_RENDER_FAILED +} = CONSTANTS.EVENTS + +const SERVER_EVENTS = { + AUCTION: 'auction', + WON: 'bidWon', + AD_RENDER_FAILED: 'adRenderFailed' +} + +const SERVER_BID_STATUS = { + NO_BID: 'noBid', + BID_REQUESTED: 'bidRequested', + BID_RECEIVED: 'bidReceived', + BID_TIMEDOUT: 'bidTimedOut', + BID_WON: 'bidWon' +} + +let auctions = {} +let initOptions = {} +let eventsQueue = [] + +const onAuctionInit = (args) => { + const { auctionId, adUnits, timestamp } = args + + let auction = auctions[auctionId] = { + ...args, + adUnits: {}, + auctionStart: timestamp, + _sentToServer: false + } + + handleCustomFields(auction, AUCTION_INIT, args) + + utils._each(adUnits, adUnit => { + auction.adUnits[adUnit.code] = { + ...adUnit, + auctionId, + adunid: adUnit.code, + bids: {}, + } + }) +} + +const onBidRequested = (args) => { + const { auctionId, bids, start, timeout } = args + const _start = start || Date.now() + const auction = auctions[auctionId] + const auctionAdUnits = auction.adUnits + + bids.forEach(bid => { + const { adUnitCode } = bid + const bidId = parseBidId(bid) + + auctionAdUnits[adUnitCode].bids[bidId] = { + ...bid, + timeout, + start: _start, + rs: _start - auction.auctionStart, + bidStatus: SERVER_BID_STATUS.BID_REQUESTED, + } + }) +} + +const onBidResponse = (args) => { + const { auctionId, adUnitCode } = args + const auction = auctions[auctionId] + const bidId = parseBidId(args) + let bid = auction.adUnits[adUnitCode].bids[bidId] + + Object.assign(bid, args, { + bidStatus: SERVER_BID_STATUS.BID_RECEIVED, + end: args.responseTimestamp, + re: args.responseTimestamp - auction.auctionStart + }) +} + +const onNoBid = (args) => { + const { auctionId, adUnitCode } = args + const bidId = parseBidId(args) + const end = Date.now() + const auction = auctions[auctionId] + let bid = auction.adUnits[adUnitCode].bids[bidId] + + Object.assign(bid, args, { + bidStatus: SERVER_BID_STATUS.NO_BID, + end, + re: end - auction.auctionStart + }) +} + +const onBidWon = (args) => { + const { auctionId, adUnitCode } = args + const bidId = parseBidId(args) + const bid = auctions[auctionId].adUnits[adUnitCode].bids[bidId] + + Object.assign(bid, args, { + bidStatus: SERVER_BID_STATUS.BID_WON, + isW: true, + isH: true + }) + + if (auctions[auctionId]._sentToServer) { + const payload = { + auctionId, + adunid: adUnitCode, + bid: mapBid(bid, BID_WON) + } + + sendEvent(SERVER_EVENTS.WON, payload) + } +} + +const onBidTimeout = (args) => { + utils._each(args, bid => { + const { auctionId, adUnitCode } = bid + const bidId = parseBidId(bid) + let bidCache = auctions[auctionId].adUnits[adUnitCode].bids[bidId] + + Object.assign(bidCache, bid, { + bidStatus: SERVER_BID_STATUS.BID_TIMEDOUT, + }) + }) +} + +const onAuctionEnd = (args) => { + const { auctionId, adUnits, ...restAuctionEnd } = args + + Object.assign(auctions[auctionId], restAuctionEnd, { + auctionEnd: args.auctionEnd || Date.now() + }) + + // wait for bidWon before sending to server + setTimeout(() => { + auctions[auctionId]._sentToServer = true + const finalAuctionData = buildAuctionData(auctions[auctionId]) + + sendEvent(SERVER_EVENTS.AUCTION, finalAuctionData) + }, initOptions.serverConfig.BID_WON_TIMEOUT || AUCTION_END_SEND_TIMEOUT) +} + +const onAdRenderFailed = (args) => { + const data = utils.deepClone(args) + data.timestamp = Date.now() + + if (data.bid) { + data.bid = mapBid(data.bid, AD_RENDER_FAILED) + } + + sendEvent(SERVER_EVENTS.AD_RENDER_FAILED, data) +} + +var ooloAdapter = Object.assign( + adapter({ analyticsType }), { + track({ eventType, args }) { + // wait for server configuration before processing the events + if (typeof initOptions.serverConfig !== 'undefined' && eventsQueue.length === 0) { + handleEvent(eventType, args) + } else { + eventsQueue.push({ eventType, args }) + } + } + } +) + +function handleEvent(eventType, args) { + try { + const { sendRaw } = initOptions.serverConfig.events[eventType] + if (sendRaw) { + sendEvent(eventType, args, true) + } + } catch (e) { } + + switch (eventType) { + case AUCTION_INIT: + onAuctionInit(args) + break + case BID_REQUESTED: + onBidRequested(args) + break + case NO_BID: + onNoBid(args) + break + case BID_RESPONSE: + onBidResponse(args) + break + case BID_WON: + onBidWon(args) + break + case BID_TIMEOUT: + onBidTimeout(args) + break + case AUCTION_END: + onAuctionEnd(args) + break + case AD_RENDER_FAILED: + onAdRenderFailed(args) + break + } +} + +function sendEvent(eventType, args, isRaw) { + let data = utils.deepClone(args) + + Object.assign(data, buildCommonDataProperties(), { + eventType + }) + + if (isRaw) { + let rawEndpoint + try { + const { endpoint, omitRawFields } = initOptions.serverConfig.events[eventType] + rawEndpoint = endpoint + handleCustomRawFields(data, omitRawFields) + } catch (e) { } + ajaxCall(rawEndpoint || ENDPOINTS.RAW, () => { }, JSON.stringify(data)) + } else { + let endpoint + if (eventType === SERVER_EVENTS.AD_RENDER_FAILED) { + endpoint = ENDPOINTS.AD_RENDER_FAILED + } else if (eventType === SERVER_EVENTS.WON) { + endpoint = ENDPOINTS.BID_WON + } else { + endpoint = ENDPOINTS.AUCTION + } + + ajaxCall(endpoint, () => { }, JSON.stringify(data)) + } +} + +function checkEventsQueue() { + while (eventsQueue.length) { + const event = eventsQueue.shift() + handleEvent(event.eventType, event.args) + } +} + +function buildAuctionData(auction) { + const auctionData = utils.deepClone(auction) + const keysToRemove = ['adUnitCodes', 'auctionStatus', 'bidderRequests', 'bidsReceived', 'noBids', 'winningBids', 'timestamp', 'config'] + + keysToRemove.forEach(key => { + delete auctionData[key] + }) + + handleCustomFields(auctionData, AUCTION_END, auction) + + // turn bids object into array of objects + Object.keys(auctionData.adUnits).forEach(adUnit => { + const adUnitObj = auctionData.adUnits[adUnit] + adUnitObj.bids = Object.keys(adUnitObj.bids).map(key => mapBid(adUnitObj.bids[key])) + delete adUnitObj['adUnitCode'] + delete adUnitObj['code'] + delete adUnitObj['transactionId'] + }) + + // turn adUnits objects into array of adUnits + auctionData.adUnits = Object.keys(auctionData.adUnits).map(key => auctionData.adUnits[key]) + + return auctionData +} + +function buildCommonDataProperties() { + return { + pvid: PAGEVIEW_ID, + pid: initOptions.pid, + pbModuleVersion + } +} + +function buildLogMessage(message) { + return `oolo: ${message}` +} + +function parseBidId(bid) { + return bid.bidId || bid.requestId +} + +function mapBid({ + bidStatus, + start, + end, + mediaType, + creativeId, + originalCpm, + originalCurrency, + source, + netRevenue, + currency, + width, + height, + timeToRespond, + responseTimestamp, + ...rest +}, eventType) { + const bidObj = { + bst: bidStatus, + s: start, + e: responseTimestamp || end, + mt: mediaType, + crId: creativeId, + oCpm: originalCpm, + oCur: originalCurrency, + src: source, + nrv: netRevenue, + cur: currency, + w: width, + h: height, + ttr: timeToRespond, + ...rest, + } + + delete bidObj['bidRequestsCount'] + delete bidObj['bidderRequestId'] + delete bidObj['bidderRequestsCount'] + delete bidObj['bidderWinsCount'] + delete bidObj['schain'] + delete bidObj['refererInfo'] + delete bidObj['statusMessage'] + delete bidObj['status'] + delete bidObj['adUrl'] + delete bidObj['ad'] + delete bidObj['usesGenericKeys'] + delete bidObj['requestTimestamp'] + + try { + handleCustomFields(bidObj, eventType || BID_RESPONSE, rest) + } catch (e) { } + + return bidObj +} + +function handleCustomFields(obj, eventType, args) { + try { + const { pickFields, omitFields } = initOptions.serverConfig.events[eventType] + + if (pickFields && obj && args) { + Object.assign(obj, utils.pick(args, pickFields)) + } + + if (omitFields && obj && args) { + omitFields.forEach(field => { + utils.deepSetValue(obj, field, undefined) + }) + } + } catch (e) { } +} + +function handleCustomRawFields(obj, omitRawFields) { + try { + if (omitRawFields && obj) { + omitRawFields.forEach(field => { + utils.deepSetValue(obj, field, undefined) + }) + } + } catch (e) { } +} + +function getServerConfig() { + const defaultConfig = { events: {} } + + ajaxCall( + ENDPOINTS.CONFIG + '?pid=' + initOptions.pid, + { + success: function (data) { + try { + initOptions.serverConfig = JSON.parse(data) || defaultConfig + } catch (e) { + initOptions.serverConfig = defaultConfig + } + checkEventsQueue() + }, + error: function () { + initOptions.serverConfig = defaultConfig + checkEventsQueue() + } + }, + null + ) +} + +function sendPage() { + setTimeout(() => { + const payload = { + timestamp: Date.now(), + screenWidth: window.screen.width, + screenHeight: window.screen.height, + url: window.location.href, + protocol: window.location.protocol, + origin: utils.getOrigin(), + referrer: getTopWindowReferrer(), + pbVersion: prebidVersion, + } + + Object.assign(payload, buildCommonDataProperties(), getPagePerformance()) + ajaxCall(ENDPOINTS.PAGE_DATA, () => { }, JSON.stringify(payload)) + }, 0) +} + +function sendHbConfigData() { + const conf = {} + const pbjsConfig = config.getConfig() + + Object.keys(pbjsConfig).forEach(key => { + if (key[0] !== '_') { + conf[key] = pbjsConfig[key] + } + }) + + ajaxCall(ENDPOINTS.HBCONFIG, () => { }, JSON.stringify(conf)) +} + +function getPagePerformance() { + let timing + + try { + timing = window.top.performance.timing + } catch (e) { } + + if (!timing) { + return + } + + const { navigationStart, domContentLoadedEventEnd, loadEventEnd } = timing + const domContentLoadTime = domContentLoadedEventEnd - navigationStart + const pageLoadTime = loadEventEnd - navigationStart + + return { + domContentLoadTime, + pageLoadTime, + } +} + +function getTopWindowReferrer() { + try { + return window.top.document.referrer + } catch (e) { + return '' + } +} + +function generatePageViewId(min = 10000, max = 90000) { + var randomNumber = Math.floor((Math.random() * max) + min) + var currentdate = new Date() + var currentTime = { + getDate: currentdate.getDate(), + getMonth: currentdate.getMonth(), + getFullYear: currentdate.getFullYear(), + getHours: currentdate.getHours(), + getMinutes: currentdate.getMinutes(), + getSeconds: currentdate.getSeconds(), + getMilliseconds: currentdate.getMilliseconds() + } + return ((currentTime.getDate <= 9) ? '0' + (currentTime.getDate) : (currentTime.getDate)) + '' + + (currentTime.getMonth + 1 <= 9 ? '0' + (currentTime.getMonth + 1) : (currentTime.getMonth + 1)) + '' + + currentTime.getFullYear % 100 + '' + + (currentTime.getHours <= 9 ? '0' + currentTime.getHours : currentTime.getHours) + '' + + (currentTime.getMinutes <= 9 ? '0' + currentTime.getMinutes : currentTime.getMinutes) + '' + + (currentTime.getSeconds <= 9 ? '0' + currentTime.getSeconds : currentTime.getSeconds) + '' + + (currentTime.getMilliseconds % 100 <= 9 ? '0' + (currentTime.getMilliseconds % 100) : (currentTime.getMilliseconds % 100)) + '' + + (randomNumber) +} + +function ajaxCall(endpoint, callback, data, options = {}) { + if (data) { + options.contentType = 'application/json' + } + + return ajax(endpoint, callback, data, options) +} + +ooloAdapter.originEnableAnalytics = ooloAdapter.enableAnalytics +ooloAdapter.enableAnalytics = function (config) { + ooloAdapter.originEnableAnalytics(config) + initOptions = config ? config.options : {} + + if (!initOptions.pid) { + utils.logError(buildLogMessage('enableAnalytics missing config object with "pid"')) + return + } + + getServerConfig() + sendHbConfigData() + + if (document.readyState === 'complete') { + sendPage() + } else { + window.addEventListener('load', sendPage) + } + + utils.logInfo(buildLogMessage('enabled analytics adapter'), config) + ooloAdapter.enableAnalytics = function () { + utils.logInfo(buildLogMessage('Analytics adapter already enabled..')) + } +} + +ooloAdapter.originDisableAnalytics = ooloAdapter.disableAnalytics +ooloAdapter.disableAnalytics = function () { + auctions = {} + initOptions = {} + ooloAdapter.originDisableAnalytics() +} + +adapterManager.registerAnalyticsAdapter({ + adapter: ooloAdapter, + code: ADAPTER_CODE +}) + +// export for testing +export { + buildAuctionData, + generatePageViewId +} + +export default ooloAdapter diff --git a/modules/ooloAnalyticsAdapter.md b/modules/ooloAnalyticsAdapter.md new file mode 100644 index 00000000000..1ffb9cbe050 --- /dev/null +++ b/modules/ooloAnalyticsAdapter.md @@ -0,0 +1,24 @@ +# Overview + +Module Name: oolo Analytics Adapter +Module Type: Analytics Adapter +Maintainer: admin@oolo.io + +# Description + +Analytics adapter for oolo. + +oolo is an anomaly detection based monitoring solution for web publishers, built by adops for adops. Its purpose is to eliminate most of the daily manual work that teams are required to do, while increasing the overall performance, due to full & faster detection of problems and opportunities. + +Contact admin@oolo.io for information. + +# Usage + +```javascript +pbjs.enableAnalytics({ + provider: 'oolo', + options: { + pid: 12345 // id provided by oolo + } +}) +``` diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index 2d6a2d6d6e5..7587962c62b 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -261,11 +261,12 @@ export const parrableIdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @param {ConsentData} [consentData] * @returns {function(callback:function), id:ParrableId} */ - getId(configParams, gdprConsentData, currentStoredId) { + getId(config, gdprConsentData, currentStoredId) { + const configParams = (config && config.params) || {}; return fetchId(configParams); } }; diff --git a/modules/pubCommonId.js b/modules/pubCommonId.js index 174fa6ffe6e..427f775c44b 100644 --- a/modules/pubCommonId.js +++ b/modules/pubCommonId.js @@ -212,9 +212,11 @@ export function requestBidHook(next, config) { // into bid requests later. if (adUnits && pubcid) { adUnits.forEach((unit) => { - unit.bids.forEach((bid) => { - Object.assign(bid, {crumbs: {pubcid}}); - }); + if (unit.bids && utils.isArray(unit.bids)) { + unit.bids.forEach((bid) => { + Object.assign(bid, {crumbs: {pubcid}}); + }); + } }); } diff --git a/modules/pubCommonIdSystem.js b/modules/pubCommonIdSystem.js index 9516934de42..51b4335fe60 100644 --- a/modules/pubCommonIdSystem.js +++ b/modules/pubCommonIdSystem.js @@ -8,13 +8,150 @@ import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; +import {ajax} from '../src/ajax.js'; const PUB_COMMON_ID = 'PublisherCommonId'; - const MODULE_NAME = 'pubCommonId'; +const COOKIE = 'cookie'; +const LOCAL_STORAGE = 'html5'; +const SHAREDID_OPT_OUT_VALUE = '00000000000000000000000000'; +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 storage = getStorageManager(null, 'pubCommonId'); +/** + * Store sharedid in either cookie or local storage + * @param {Object} config Need config.storage object to derive key, expiry time, and storage type. + * @param {string} value Shareid value to store + */ + +function storeData(config, value) { + try { + if (value) { + const key = config.storage.name + SHAREDID_SUFFIX; + const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString(); + + if (config.storage.type === COOKIE) { + if (storage.cookiesAreEnabled()) { + storage.setCookie(key, value, expiresStr, 'LAX', COOKIE_DOMAIN); + } + } else if (config.storage.type === LOCAL_STORAGE) { + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage(`${key}_exp`, expiresStr); + storage.setDataInLocalStorage(key, value); + } + } + } + } catch (error) { + utils.logError(error); + } +} + +/** + * Read sharedid from cookie or local storage + * @param config Need config.storage to derive key and storage type + * @return {string} + */ +function readData(config) { + try { + const key = config.storage.name + SHAREDID_SUFFIX; + if (config.storage.type === COOKIE) { + if (storage.cookiesAreEnabled()) { + return storage.getCookie(key); + } + } else if (config.storage.type === LOCAL_STORAGE) { + if (storage.hasLocalStorage()) { + const expValue = storage.getDataFromLocalStorage(`${key}_exp`); + if (!expValue) { + return storage.getDataFromLocalStorage(key); + } else if ((new Date(expValue)).getTime() - Date.now() > 0) { + return storage.getDataFromLocalStorage(key) + } + } + } + } catch (error) { + utils.logError(error); + } +} + +/** + * Delete sharedid from cookie or local storage + * @param config Need config.storage to derive key and storage type + */ +function delData(config) { + try { + const key = config.storage.name + SHAREDID_SUFFIX; + if (config.storage.type === COOKIE) { + if (storage.cookiesAreEnabled()) { + storage.setCookie(key, '', EXPIRED_COOKIE_DATE); + } + } else if (config.storage.type === LOCAL_STORAGE) { + storage.removeDataFromLocalStorage(`${key}_exp`); + storage.removeDataFromLocalStorage(key); + } + } catch (error) { + utils.logError(error); + } +} + +/** + * setup success and error handler for sharedid callback thru ajax + * @param {string} pubcid Current pubcommon id + * @param {function} callback userId module callback. + * @param {Object} config Need config.storage to derive sharedid storage params + * @return {{success: success, error: error}} + */ + +function handleResponse(pubcid, callback, config) { + return { + success: function (responseBody) { + if (responseBody) { + try { + let responseObj = JSON.parse(responseBody); + utils.logInfo('PubCommonId: Generated SharedId: ' + responseObj.sharedId); + if (responseObj.sharedId) { + if (responseObj.sharedId !== SHAREDID_OPT_OUT_VALUE) { + // Store sharedId locally + storeData(config, responseObj.sharedId); + } else { + // Delete local copy if the user has opted out + delData(config); + } + } + // Pass pubcid even though there is no change in order to trigger decode + callback(pubcid); + } catch (error) { + utils.logError(error); + } + } + }, + error: function (statusText, responseBody) { + utils.logInfo('PubCommonId: failed to get sharedid'); + } + } +} + +/** + * Wraps pixelCallback in order to call sharedid sync + * @param {string} pubcid Pubcommon id value + * @param {function|undefined} pixelCallback fires a pixel to first party server + * @param {Object} config Need config.storage to derive sharedid storage params. + * @return {function(...[*]=)} + */ + +function getIdCallback(pubcid, pixelCallback, config) { + return function (callback) { + if (typeof pixelCallback === 'function') { + pixelCallback(); + } + ajax(SHAREDID_URL, handleResponse(pubcid, callback, config), undefined, {method: 'GET', withCredentials: true}); + } +} + /** @type {Submodule} */ export const pubCommonIdSubmodule = { /** @@ -22,6 +159,7 @@ export const pubCommonIdSubmodule = { * @type {string} */ name: MODULE_NAME, + /** * Return a callback function that calls the pixelUrl with id as a query parameter * @param pixelUrl @@ -46,52 +184,93 @@ export const pubCommonIdSubmodule = { * decode the stored id value for passing to bid requests * @function * @param {string} value + * @param {SubmoduleConfig} config * @returns {{pubcid:string}} */ - decode(value) { - return { 'pubcid': value } + decode(value, config) { + const idObj = {'pubcid': value}; + const {params: {enableSharedId = SHAREDID_DEFAULT_STATE} = {}} = config; + + if (enableSharedId) { + const sharedId = readData(config); + if (sharedId) idObj['sharedid'] = {id: sharedId}; + } + + return idObj; }, /** * performs action to obtain id * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] Config object with params and storage properties + * @param {Object} consentData + * @param {string} storedId Existing pubcommon id * @returns {IdResponse} */ - getId: function ({create = true, pixelUrl} = {}) { - try { - if (typeof window[PUB_COMMON_ID] === 'object') { - // If the page includes its own pubcid module, then save a copy of id. - return {id: window[PUB_COMMON_ID].getId()}; + getId: function (config = {}, consentData, storedId) { + const {params: {create = true, pixelUrl, enableSharedId = SHAREDID_DEFAULT_STATE} = {}} = config; + let newId = storedId; + if (!newId) { + try { + if (typeof window[PUB_COMMON_ID] === 'object') { + // If the page includes its own pubcid module, then save a copy of id. + newId = window[PUB_COMMON_ID].getId(); + } + } catch (e) { } - } catch (e) { - } - const newId = (create && utils.hasDeviceAccess()) ? utils.generateUUID() : undefined; - return { - id: newId, - callback: this.makeCallback(pixelUrl, newId) + if (!newId) newId = (create && utils.hasDeviceAccess()) ? utils.generateUUID() : undefined; } + + const pixelCallback = this.makeCallback(pixelUrl, newId); + const combinedCallback = enableSharedId ? getIdCallback(newId, pixelCallback, config) : pixelCallback; + + return {id: newId, callback: combinedCallback}; }, /** - * performs action to extend an id + * performs action to extend an id. There are generally two ways to extend the expiration time + * of stored id: using pixelUrl or return the id and let main user id module write it again with + * the new expiration time. + * + * PixelUrl, if defined, should point back to a first party domain endpoint. On the server + * side, there is either a plugin, or customized logic to read and write back the pubcid cookie. + * The extendId function itself should return only the callback, and not the id itself to avoid + * having the script-side overwriting server-side. This applies to both pubcid and sharedid. + * + * On the other hand, if there is no pixelUrl, then the extendId should return storedId so that + * its expiration time is updated. Sharedid, however, will have to be updated by this submodule + * separately. + * * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleParams} [config] * @param {Object} storedId existing id * @returns {IdResponse|undefined} */ - extendId: function({extend = false, pixelUrl} = {}, storedId) { - try { - if (typeof window[PUB_COMMON_ID] === 'object') { - // If the page includes its onw pubcid module, then there is nothing to do. - return; - } - } catch (e) { - } + extendId: function(config = {}, storedId) { + const {params: {extend = false, pixelUrl, enableSharedId = SHAREDID_DEFAULT_STATE} = {}} = config; if (extend) { - // When extending, only one of response fields is needed - const callback = this.makeCallback(pixelUrl, storedId); - return callback ? {callback: callback} : {id: storedId}; + try { + if (typeof window[PUB_COMMON_ID] === 'object') { + if (enableSharedId) { + // If the page includes its own pubcid module, then there is nothing to do + // except to update sharedid's expiration time + storeData(config, readData(config)); + } + return; + } + } catch (e) { + } + + if (pixelUrl) { + const callback = this.makeCallback(pixelUrl, storedId); + return {callback: callback}; + } else { + if (enableSharedId) { + // Update with the same value to extend expiration time + storeData(config, readData(config)); + } + return {id: storedId}; + } } }, @@ -123,4 +302,6 @@ export const pubCommonIdSubmodule = { } }; +const COOKIE_DOMAIN = pubCommonIdSubmodule.domainOverride(); + submodule('userId', pubCommonIdSubmodule); diff --git a/modules/pubProvidedSystem.js b/modules/pubProvidedIdSystem.js similarity index 91% rename from modules/pubProvidedSystem.js rename to modules/pubProvidedIdSystem.js index 575633e622f..0b2175f57cb 100644 --- a/modules/pubProvidedSystem.js +++ b/modules/pubProvidedIdSystem.js @@ -34,10 +34,11 @@ export const pubProvidedIdSubmodule = { /** * performs action to obtain id and return a value. * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {{id: array}} */ - getId(configParams) { + getId(config) { + const configParams = (config && config.params) || {}; let res = []; if (utils.isArray(configParams.eids)) { res = res.concat(configParams.eids); diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index 04c6606a299..c7eeaf87fdc 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -152,6 +152,7 @@ function parseBidResponse(bid) { 'mediaType', 'params', 'mi', + 'regexPattern', () => bid.regexPattern || undefined, 'partnerImpId', // partner impression ID 'dimensions', () => utils.pick(bid, [ 'width', @@ -166,6 +167,35 @@ function getDomainFromUrl(url) { return a.hostname; } +function getDevicePlatform() { + var deviceType = 3; + try { + var ua = navigator.userAgent; + if (ua && utils.isStr(ua) && ua.trim() != '') { + ua = ua.toLowerCase().trim(); + var isMobileRegExp = new RegExp('(mobi|tablet|ios).*'); + if (ua.match(isMobileRegExp)) { + deviceType = 2; + } else { + deviceType = 1; + } + } + } catch (ex) {} + return deviceType; +} + +function getValueForKgpv(bid, adUnitId) { + if (bid.params.regexPattern) { + return bid.params.regexPattern; + } else if (bid.bidResponse && bid.bidResponse.regexPattern) { + return bid.bidResponse.regexPattern; + } else if (bid.params.kgpv) { + return bid.params.kgpv; + } else { + return adUnitId; + } +} + function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { highestBid = (highestBid && highestBid.length > 0) ? highestBid[0] : null; return Object.keys(adUnit.bids).reduce(function(partnerBids, bidId) { @@ -174,7 +204,7 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'pn': bid.bidder, 'bidid': bid.bidId, 'db': bid.bidResponse ? 0 : 1, - 'kgpv': bid.params.kgpv ? bid.params.kgpv : adUnitId, + 'kgpv': getValueForKgpv(bid, adUnitId), 'kgpsv': bid.params.kgpv ? bid.params.kgpv : adUnitId, 'psz': bid.bidResponse ? (bid.bidResponse.dimensions.width + 'x' + bid.bidResponse.dimensions.height) : '0x0', 'eg': bid.bidResponse ? bid.bidResponse.bidGrossCpmUSD : 0, @@ -220,6 +250,7 @@ function executeBidsLoggerCall(e, highestCpmBids) { outputObj['tst'] = Math.round((new window.Date()).getTime() / 1000); outputObj['pid'] = '' + profileId; outputObj['pdvid'] = '' + profileVersionId; + outputObj['dvc'] = {'plt': getDevicePlatform()}; outputObj['tgid'] = (function() { var testGroupId = parseInt(config.getConfig('testGroupId') || 0); if (testGroupId <= 15 && testGroupId >= 0) { @@ -268,7 +299,7 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { pixelURL += '&pn=' + enc(winningBid.bidder); pixelURL += '&en=' + enc(winningBid.bidResponse.bidPriceUSD); pixelURL += '&eg=' + enc(winningBid.bidResponse.bidGrossCpmUSD); - pixelURL += '&kgpv=' + enc(winningBid.params.kgpv || adUnitId); + pixelURL += '&kgpv=' + enc(getValueForKgpv(winningBid, adUnitId)); pixelURL += '&piid=' + enc(winningBid.bidResponse.partnerImpId || EMPTY_STRING); ajax( pixelURL, diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js index b3b8a647137..bc2854de40b 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -68,7 +68,7 @@ function buildRequests(validBidRequests, bidderRequest) { payload.width = playerSize[0][0]; payload.height = playerSize[0][1]; } else if (hasBannerMediaType(bidRequest)) { - const sizes = utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes'); + const sizes = getValidSizes(utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes')); payload.width = sizes[0][0]; payload.height = sizes[0][1]; } @@ -234,15 +234,9 @@ function isBannerValid(bid) { if (!isMobile()) { return false; } - const sizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes'); - if (sizes && utils.isArray(sizes)) { - if (utils.isArray(sizes[0])) { - const width = sizes[0][0]; - const height = sizes[0][1]; - if (width >= 300 && height >= 250) { - return true; - } - } + const sizes = getValidSizes(utils.deepAccess(bid, 'mediaTypes.banner.sizes')); + if (sizes.length > 0) { + return true; } return false; } @@ -291,6 +285,22 @@ function hasVideoMediaType(bid) { return !!utils.deepAccess(bid, 'mediaTypes.video'); } +function getValidSizes(sizes) { + let result = []; + if (sizes && utils.isArray(sizes) && sizes.length > 0) { + for (let i = 0; i < sizes.length; i++) { + if (utils.isArray(sizes[i]) && sizes[i].length == 2) { + const width = sizes[i][0]; + const height = sizes[i][1]; + if ((width >= 300 && height >= 250) || (width == 1 && height == 1)) { + result.push([width, height]); + } + } + } + } + return result; +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index a446cbc3ce0..069a7e7ead5 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -102,7 +102,8 @@ var sizeMap = { 274: '1800x200', 278: '320x500', 282: '320x400', - 288: '640x380' + 288: '640x380', + 548: '500x1000' }; utils._each(sizeMap, (item, key) => sizeMap[item] = key); @@ -236,8 +237,7 @@ export const spec = { const eids = utils.deepAccess(bidderRequest, 'bids.0.userIdAsEids'); if (eids && eids.length) { - // filter out unsupported id systems - utils.deepSetValue(data, 'user.ext.eids', eids.filter(eid => ['adserver.org', 'pubcid.org', 'liveintent.com', 'liveramp.com', 'sharedid.org', 'criteo.com'].indexOf(eid.source) !== -1)); + 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'); @@ -389,6 +389,10 @@ export const spec = { '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', @@ -519,6 +523,10 @@ export const spec = { 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}`; diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index 5c2a3df0595..cdd840c4f54 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -296,10 +296,10 @@ export const sharedIdSubmodule = { /** * performs action to obtain id and return a value. * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {sharedId} */ - getId(configParams) { + getId(config) { const resp = function (callback) { utils.logInfo('SharedId: Sharedid doesnt exists, new cookie creation'); ajax(ID_SVC, idGenerationCallback(callback), undefined, {method: 'GET', withCredentials: true}); @@ -309,11 +309,12 @@ export const sharedIdSubmodule = { /** * performs actions even if the id exists and returns a value - * @param configParams + * @param config * @param storedId * @returns {{callback: *}} */ - extendId(configParams, storedId) { + extendId(config, storedId) { + const configParams = (config && config.params) || {}; utils.logInfo('SharedId: Existing shared id ' + storedId.id); const resp = function (callback) { const needSync = isIdSynced(configParams, storedId); diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index 4df537e0eb3..ffd242e57ac 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -66,7 +66,7 @@ export function isUsingNewSizeMapping(adUnits) { }); // checks for the presence of sizeConfig property at the adUnit.bids[].bidder object - adUnit.bids.forEach(bidder => { + adUnit.bids && utils.isArray(adUnit.bids) && adUnit.bids.forEach(bidder => { if (bidder.sizeConfig) { if (isUsingSizeMappingBool === false) { isUsingSizeMappingBool = true; @@ -168,8 +168,15 @@ export function checkAdUnitSetupHook(adUnits) { } const validatedAdUnits = []; adUnits.forEach(adUnit => { + const bids = adUnit.bids; const mediaTypes = adUnit.mediaTypes; let validatedBanner, validatedVideo, validatedNative; + + if (!bids || !utils.isArray(bids)) { + utils.logError(`Detected adUnit.code '${adUnit.code}' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.`); + return; + } + if (!mediaTypes || Object.keys(mediaTypes).length === 0) { utils.logError(`Detected adUnit.code '${adUnit.code}' did not have a 'mediaTypes' object defined. This is a required field for the auction, so this adUnit has been removed.`); return; diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index 97dd43fc5ba..8462e749b91 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -83,10 +83,11 @@ export const spec = { w: size[0], h: size[1] })); - } else if (videoMediaType && videoMediaType.context === 'instream') { + } else if (videoMediaType && (videoMediaType.context === 'instream' || videoMediaType.context === 'outstream')) { // Specific attributes for instream. let playerSize = videoMediaType.playerSize[0]; - payload.isVideo = true; + payload.isVideo = videoMediaType.context === 'instream'; + payload.mediaType = VIDEO; payload.videoData = { videoProtocol: bid.params.video.protocol, playerWidth: playerSize[0], @@ -146,10 +147,11 @@ export const spec = { ttl: response.ttl }; - if (bidRequest.isVideo) { + if (bidRequest.mediaType === VIDEO) { bidResponse.mediaType = VIDEO; bidResponse.vastUrl = response.adUrl; bidResponse.vastXml = response.ad; + bidResponse.content = response.ad; } else { bidResponse.adUrl = response.adUrl; bidResponse.ad = response.ad; diff --git a/modules/smartadserverBidAdapter.md b/modules/smartadserverBidAdapter.md index c6f68363d7c..05e29359fd2 100644 --- a/modules/smartadserverBidAdapter.md +++ b/modules/smartadserverBidAdapter.md @@ -94,4 +94,43 @@ Please reach out to your Technical account manager for more information. } }] }; +``` + +## Outstream Video + +``` + var outstreamVideoAdUnit = { + code: 'test-div', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + render: function (bid) { + bid.renderer.push(() => { + ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: bid + }); + }); + } + }, + bids: [{ + bidder: "smart", + params: { + domain: 'https://prg.smartadserver.com', + siteId: 207435, + pageId: 896536, + formatId: 85089, + bidfloor: 5, + video: { + protocol: 6, + startDelay: 1 + } + } + }] + }; ``` \ No newline at end of file diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index f3260668b74..935b0ceb489 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,10 +1,12 @@ import * as utils from '../src/utils.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER } from '../src/mediaTypes.js' +import { createEidsArray } from './userId/eids.js'; export const spec = { code: 'sovrn', supportedMediaTypes: [BANNER], + gvlid: 13, /** * Check if the bid is a valid zone ID in either number or string form @@ -25,11 +27,21 @@ export const spec = { let sovrnImps = []; let iv; let schain; - let unifiedID; + let eids; + let tpid = [] + let criteoId; utils._each(bidReqs, function (bid) { - if (!unifiedID) { - unifiedID = utils.deepAccess(bid, 'userId.tdid'); + if (!eids && bid.userId) { + eids = createEidsArray(bid.userId) + eids.forEach(function (id) { + if (id.uids && id.uids[0]) { + if (id.source === 'criteo.com') { + criteoId = id.uids[0].id + } + tpid.push({source: id.source, uid: id.uids[0].id}) + } + }) } if (bid.schain) { @@ -84,19 +96,12 @@ export const spec = { utils.deepSetValue(sovrnBidReq, 'regs.ext.us_privacy', bidderRequest.uspConsent); } - if (unifiedID) { - const idArray = [{ - source: 'adserver.org', - uids: [ - { - id: unifiedID, - ext: { - rtiPartner: 'TDID' - } - } - ] - }] - utils.deepSetValue(sovrnBidReq, 'user.ext.eids', idArray) + if (eids) { + utils.deepSetValue(sovrnBidReq, 'user.ext.eids', eids) + utils.deepSetValue(sovrnBidReq, 'user.ext.tpid', tpid) + if (criteoId) { + utils.deepSetValue(sovrnBidReq, 'user.ext.prebid_criteoid', criteoId) + } } let url = `https://ap.lijit.com/rtb/bid?src=$$REPO_AND_VERSION$$`; diff --git a/modules/stroeerCoreBidAdapter.js b/modules/stroeerCoreBidAdapter.js new file mode 100644 index 00000000000..ec442f5125a --- /dev/null +++ b/modules/stroeerCoreBidAdapter.js @@ -0,0 +1,196 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const GVL_ID = 136; +const BIDDER_CODE = 'stroeerCore'; +const DEFAULT_HOST = 'hb.adscale.de'; +const DEFAULT_PATH = '/dsh'; +const DEFAULT_PORT = ''; +const FIVE_MINUTES_IN_SECONDS = 300; +const USER_SYNC_IFRAME_URL = 'https://js.adscale.de/pbsync.html'; + +const isSecureWindow = () => utils.getWindowSelf().location.protocol === 'https:'; +const isMainPageAccessible = () => getMostAccessibleTopWindow() === utils.getWindowTop(); + +function getTopWindowReferrer() { + try { + return utils.getWindowTop().document.referrer; + } catch (e) { + return utils.getWindowSelf().referrer; + } +} + +function getMostAccessibleTopWindow() { + let res = utils.getWindowSelf(); + + try { + while (utils.getWindowTop().top !== res && res.parent.location.href.length) { + res = res.parent; + } + } catch (ignore) { + } + + return res; +} + +function elementInView(elementId) { + const resolveElement = (elId) => { + const win = utils.getWindowSelf(); + + return win.document.getElementById(elId); + }; + + const visibleInWindow = (el, win) => { + const rect = el.getBoundingClientRect(); + const inView = (rect.top + rect.height >= 0) && (rect.top <= win.innerHeight); + + if (win !== win.parent) { + return inView && visibleInWindow(win.frameElement, win.parent); + } + + return inView; + }; + + try { + return visibleInWindow(resolveElement(elementId), utils.getWindowSelf()); + } catch (e) { + // old browser, element not found, cross-origin etc. + } + return undefined; +} + +function buildUrl({host: hostname = DEFAULT_HOST, port = DEFAULT_PORT, securePort, path: pathname = DEFAULT_PATH}) { + if (securePort) { + port = securePort; + } + + return utils.buildUrl({protocol: 'https', hostname, port, pathname}); +} + +function getGdprParams(gdprConsent) { + if (gdprConsent) { + const consentString = encodeURIComponent(gdprConsent.consentString || '') + const isGdpr = gdprConsent.gdprApplies ? 1 : 0; + + return `?gdpr=${isGdpr}&gdpr_consent=${consentString}` + } else { + return ''; + } +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVL_ID, + supportedMediaTypes: [BANNER], + + isBidRequestValid: (function () { + const validators = []; + + const createValidator = (checkFn, errorMsgFn) => { + return (bidRequest) => { + if (checkFn(bidRequest)) { + return true; + } else { + utils.logError(`invalid bid: ${errorMsgFn(bidRequest)}`, 'ERROR'); + return false; + } + } + }; + + function isBanner(bidReq) { + return (!bidReq.mediaTypes && !bidReq.mediaType) || + (bidReq.mediaTypes && bidReq.mediaTypes.banner) || + bidReq.mediaType === BANNER; + } + + validators.push(createValidator((bidReq) => isBanner(bidReq), + bidReq => `bid request ${bidReq.bidId} is not a banner`)); + validators.push(createValidator((bidReq) => typeof bidReq.params === 'object', + bidReq => `bid request ${bidReq.bidId} does not have custom params`)); + validators.push(createValidator((bidReq) => utils.isStr(bidReq.params.sid), + bidReq => `bid request ${bidReq.bidId} does not have a sid string field`)); + + return function (bidRequest) { + return validators.every(f => f(bidRequest)); + } + }()), + + buildRequests: function (validBidRequests = [], bidderRequest) { + const anyBid = bidderRequest.bids[0]; + + const payload = { + id: bidderRequest.auctionId, + bids: [], + ref: getTopWindowReferrer(), + ssl: isSecureWindow(), + mpa: isMainPageAccessible(), + timeout: bidderRequest.timeout - (Date.now() - bidderRequest.auctionStart) + }; + + const userIds = anyBid.userId; + + if (!utils.isEmpty(userIds)) { + payload.user = { + euids: userIds + }; + } + + const gdprConsent = bidderRequest.gdprConsent; + + if (gdprConsent && gdprConsent.consentString != null && gdprConsent.gdprApplies != null) { + payload.gdpr = { + consent: bidderRequest.gdprConsent.consentString, applies: bidderRequest.gdprConsent.gdprApplies + }; + } + + function bidSizes(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes /* for prebid < 3 */ || []; + } + + validBidRequests.forEach(bid => { + payload.bids.push({ + bid: bid.bidId, sid: bid.params.sid, siz: bidSizes(bid), viz: elementInView(bid.adUnitCode) + }); + }); + + return { + method: 'POST', url: buildUrl(anyBid.params), data: payload + } + }, + + interpretResponse: function (serverResponse) { + const bids = []; + + if (serverResponse.body && typeof serverResponse.body === 'object') { + serverResponse.body.bids.forEach(bidResponse => { + bids.push({ + requestId: bidResponse.bidId, + cpm: bidResponse.cpm || 0, + width: bidResponse.width || 0, + height: bidResponse.height || 0, + ad: bidResponse.ad, + ttl: FIVE_MINUTES_IN_SECONDS, + currency: 'EUR', + netRevenue: true, + creativeId: '', + }); + }); + } + + return bids; + }, + + getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { + if (serverResponses.length > 0 && syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: USER_SYNC_IFRAME_URL + getGdprParams(gdprConsent) + }]; + } + + return []; + } +}; + +registerBidder(spec); diff --git a/modules/stroeerCoreBidAdapter.md b/modules/stroeerCoreBidAdapter.md new file mode 100644 index 00000000000..fe6e92057c6 --- /dev/null +++ b/modules/stroeerCoreBidAdapter.md @@ -0,0 +1,31 @@ +## Overview + +``` +Module Name: Stroeer Bidder Adapter +Module Type: Bidder Adapter +Maintainer: help@cz.stroeer-labs.com +``` + + +## Ad unit configuration for publishers + +```javascript +const adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [{ + bidder: 'stroeerCore', + params: { + sid: "06b782cc-091b-4f53-9cd2-0291679aa1ac" + } + }] +}]; +``` +### Config Notes + +* Slot id (`sid`) is required. The adapter will ignore bid requests from prebid if `sid` is not provided. This must be in the decoded form. For example, "1234" as opposed to "MTM0ODA=". +* The server ignores dimensions that are not supported by the slot or by the platform (such as 987x123). diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 08ae1854669..6d55aabbfb5 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -133,8 +133,8 @@ function getTimeToFirstByte(win) { performance.getEntriesByType('navigation')[0] && performance.getEntriesByType('navigation')[0].responseStart && performance.getEntriesByType('navigation')[0].requestStart && - performance.getEntriesByType('navigation')[0].responseStart >= 0 && - performance.getEntriesByType('navigation')[0].requestStart >= 0 && + performance.getEntriesByType('navigation')[0].responseStart > 0 && + performance.getEntriesByType('navigation')[0].requestStart > 0 && Math.round( performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart ); @@ -146,8 +146,8 @@ function getTimeToFirstByte(win) { const ttfbWithTimingV1 = performance && performance.timing.responseStart && performance.timing.requestStart && - performance.timing.responseStart >= 0 && - performance.timing.requestStart >= 0 && + performance.timing.responseStart > 0 && + performance.timing.requestStart > 0 && performance.timing.responseStart - performance.timing.requestStart; return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; diff --git a/modules/truereachBidAdapter.js b/modules/truereachBidAdapter.js index 2de7edbc04d..92f9cc9951e 100755 --- a/modules/truereachBidAdapter.js +++ b/modules/truereachBidAdapter.js @@ -79,6 +79,26 @@ export const spec = { return bidResponses; }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + 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.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: 'http://ads.momagic.com/jsp/usersync.jsp' + gdprParams + }); + } + return syncs; + } }; diff --git a/modules/unifiedIdSystem.js b/modules/unifiedIdSystem.js index f916030d643..3db4003c424 100644 --- a/modules/unifiedIdSystem.js +++ b/modules/unifiedIdSystem.js @@ -30,10 +30,11 @@ export const unifiedIdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId(configParams) { + getId(config) { + const configParams = (config && config.params) || {}; if (!configParams || (typeof configParams.partner !== 'string' && typeof configParams.url !== 'string')) { utils.logError('User ID - unifiedId submodule requires either partner or url to be defined'); return; diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 4e4576dbb14..f6c58a5a0bf 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -150,7 +150,13 @@ const USER_IDS_CONFIG = { 'quantcastId': { source: 'quantcast.com', atype: 1 - } + }, + + // IDx + 'idx': { + source: 'idx.lat', + atype: 1 + }, }; // this function will create an eid object for the given UserId sub-module diff --git a/modules/userId/index.js b/modules/userId/index.js index 14f7ad3599b..83573be8682 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -14,7 +14,7 @@ * If IdResponse#callback is defined, then it'll called at the end of auction. * It's permissible to return neither, one, or both fields. * @name Submodule#getId - * @param {SubmoduleParams} configParams + * @param {SubmoduleConfig} config * @param {ConsentData|undefined} consentData * @param {(Object|undefined)} cacheIdObj * @return {(IdResponse|undefined)} A response object that contains id and/or callback. @@ -27,7 +27,7 @@ * If IdResponse#callback is defined, then it'll called at the end of auction. * It's permissible to return neither, one, or both fields. * @name Submodule#extendId - * @param {SubmoduleParams} configParams + * @param {SubmoduleConfig} config * @param {Object} storedId - existing id, if any * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ @@ -37,7 +37,7 @@ * @summary decode a stored value for passing to bid requests * @name Submodule#decode * @param {Object|string} value - * @param {SubmoduleParams|undefined} configParams + * @param {SubmoduleConfig|undefined} config * @return {(Object|undefined)} */ @@ -85,6 +85,7 @@ * @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 */ /** @@ -108,15 +109,20 @@ * @property {(function|undefined)} callback - function that will return an id */ +/** + * @typedef {Object} RefreshUserIdsOptions + * @property {(string[]|undefined)} submoduleNames - submodules to refresh + */ + import find from 'core-js-pure/features/array/find.js'; -import {config} from '../../src/config.js'; +import { config } from '../../src/config.js'; import events from '../../src/events.js'; import * as utils from '../../src/utils.js'; -import {getGlobal} from '../../src/prebidGlobal.js'; -import {gdprDataHandler} from '../../src/adapterManager.js'; +import { getGlobal } from '../../src/prebidGlobal.js'; +import { gdprDataHandler } from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; -import {module, hook} from '../../src/hook.js'; -import {createEidsArray} from './eids.js'; +import { module, hook } from '../../src/hook.js'; +import { createEidsArray } from './eids.js'; import { getCoreStorageManager } from '../../src/storageManager.js'; const MODULE_NAME = 'User ID'; @@ -313,8 +319,8 @@ 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() { }; - submodules.forEach(function(submodule) { + const done = cb ? utils.delayExecution(cb, submodules.length) : function () { }; + submodules.forEach(function (submodule) { submodule.callback(function callbackCompleted(idObj) { // if valid, id data should be saved to cookie/html storage if (idObj) { @@ -322,7 +328,7 @@ function processSubmoduleCallbacks(submodules, cb) { setStoredValue(submodule, idObj); } // cache decoded value (this is copied to every adUnit bid) - submodule.idObj = submodule.submodule.decode(idObj); + submodule.idObj = submodule.submodule.decode(idObj, submodule.config); } else { utils.logInfo(`${MODULE_NAME}: ${submodule.submodule.name} - request id responded with an empty value`); } @@ -365,11 +371,13 @@ function addIdDataToAdUnitBids(adUnits, submodules) { const combinedSubmoduleIdsAsEids = createEidsArray(combinedSubmoduleIds); if (Object.keys(combinedSubmoduleIds).length) { adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - // create a User ID object on the bid, - bid.userId = combinedSubmoduleIds; - bid.userIdAsEids = combinedSubmoduleIdsAsEids; - }); + if (adUnit.bids && utils.isArray(adUnit.bids)) { + adUnit.bids.forEach(bid => { + // create a User ID object on the bid, + bid.userId = combinedSubmoduleIds; + bid.userIdAsEids = combinedSubmoduleIdsAsEids; + }); + } }); } } @@ -392,7 +400,7 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { // delay auction until ids are available delayed = true; let continued = false; - const continueCallback = function() { + const continueCallback = function () { if (!continued) { continued = true; continueAuction(); @@ -409,7 +417,7 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { // when syncDelay is zero, process callbacks now, otherwise delay process with a setTimeout if (syncDelay > 0) { - setTimeout(function() { + setTimeout(function () { processSubmoduleCallbacks(submodulesWithCallbacks); }, syncDelay); } else { @@ -437,7 +445,7 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { */ export function requestBidsHook(fn, reqBidsConfigObj) { // initialize submodules only when undefined - initializeSubmodulesAndExecuteCallbacks(function() { + initializeSubmodulesAndExecuteCallbacks(function () { // pass available user id data to bid adapters addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, initializedSubmodules); // calling fn allows prebid to continue processing @@ -465,13 +473,112 @@ function getUserIdsAsEids() { return createEidsArray(getCombinedSubmoduleIds(initializedSubmodules)); } +/** +* This function will be exposed in the global-name-space so that userIds can be refreshed after initialization. +* @param {RefreshUserIdsOptions} options +*/ +function refreshUserIds(options, callback) { + let submoduleNames = options ? options.submoduleNames : null; + if (!submoduleNames) { + submoduleNames = []; + } + + initializeSubmodulesAndExecuteCallbacks(function() { + let consentData = gdprDataHandler.getConsentData() + + const storedConsentData = getStoredConsentData(); + setStoredConsentData(consentData); + + // gdpr consent with purpose one is required, otherwise exit immediately + let {userIdModules, hasValidated} = validateGdprEnforcement(submodules, consentData); + if (!hasValidated && !hasGDPRConsent(consentData)) { + utils.logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); + return; + } + + let callbackSubmodules = []; + for (let submodule of userIdModules) { + if (submoduleNames.length > 0 && + submoduleNames.indexOf(submodule.submodule.name) === -1) { + continue; + } + + utils.logInfo(`${MODULE_NAME} - refreshing ${submodule.submodule.name}`); + populateSubmoduleId(submodule, consentData, storedConsentData, true); + + if (utils.isFn(submodule.callback)) { + callbackSubmodules.push(submodule); + } + } + + if (callbackSubmodules.length > 0) { + processSubmoduleCallbacks(callbackSubmodules); + } + + if (callback) { + callback(); + } + }); +} + /** * This hook returns updated list of submodules which are allowed to do get user id based on TCF 2 enforcement rules configured */ export const validateGdprEnforcement = hook('sync', function (submodules, consentData) { - return {userIdModules: submodules, hasValidated: consentData && consentData.hasValidated}; + return { userIdModules: submodules, hasValidated: consentData && consentData.hasValidated }; }, 'validateGdprEnforcement'); +function populateSubmoduleId(submodule, consentData, storedConsentData, forceRefresh) { + // There are two submodule configuration types to handle: storage or value + // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method + // 2. value: pass directly to bids + if (submodule.config.storage) { + let storedId = getStoredValue(submodule.config.storage); + let response; + + let refreshNeeded = false; + if (typeof submodule.config.storage.refreshInSeconds === 'number') { + const storedDate = new Date(getStoredValue(submodule.config.storage, 'last')); + refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000); + } + + if (!storedId || refreshNeeded || forceRefresh || !storedConsentDataMatchesConsentData(storedConsentData, consentData)) { + // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. + response = submodule.submodule.getId(submodule.config, consentData, storedId); + } else if (typeof submodule.submodule.extendId === 'function') { + // If the id exists already, give submodule a chance to decide additional actions that need to be taken + response = submodule.submodule.extendId(submodule.config, storedId); + } + + if (utils.isPlainObject(response)) { + if (response.id) { + // A getId/extendId result assumed to be valid user id data, which should be saved to users local storage or cookies + setStoredValue(submodule, response.id); + storedId = response.id; + } + + if (typeof response.callback === 'function') { + // Save async callback to be invoked after auction + submodule.callback = response.callback; + } + } + + if (storedId) { + // cache decoded value (this is copied to every adUnit bid) + submodule.idObj = submodule.submodule.decode(storedId, submodule.config); + } + } else if (submodule.config.value) { + // cache decoded value (this is copied to every adUnit bid) + submodule.idObj = submodule.config.value; + } else { + const response = submodule.submodule.getId(submodule.config, consentData, undefined); + if (utils.isPlainObject(response)) { + if (typeof response.callback === 'function') { submodule.callback = response.callback; } + if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config); } + } + } +} + /** * @param {SubmoduleContainer[]} submodules * @param {ConsentData} consentData @@ -483,61 +590,14 @@ function initSubmodules(submodules, consentData) { setStoredConsentData(consentData); // gdpr consent with purpose one is required, otherwise exit immediately - let {userIdModules, hasValidated} = validateGdprEnforcement(submodules, consentData); + let { userIdModules, hasValidated } = validateGdprEnforcement(submodules, consentData); if (!hasValidated && !hasGDPRConsent(consentData)) { utils.logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); return []; } return userIdModules.reduce((carry, submodule) => { - // There are two submodule configuration types to handle: storage or value - // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method - // 2. value: pass directly to bids - if (submodule.config.storage) { - let storedId = getStoredValue(submodule.config.storage); - let response; - - let refreshNeeded = false; - if (typeof submodule.config.storage.refreshInSeconds === 'number') { - const storedDate = new Date(getStoredValue(submodule.config.storage, 'last')); - refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000); - } - - if (!storedId || refreshNeeded || !storedConsentDataMatchesConsentData(storedConsentData, consentData)) { - // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. - response = submodule.submodule.getId(submodule.config.params, consentData, storedId); - } else if (typeof submodule.submodule.extendId === 'function') { - // If the id exists already, give submodule a chance to decide additional actions that need to be taken - response = submodule.submodule.extendId(submodule.config.params, storedId); - } - - if (utils.isPlainObject(response)) { - if (response.id) { - // A getId/extendId result assumed to be valid user id data, which should be saved to users local storage or cookies - setStoredValue(submodule, response.id); - storedId = response.id; - } - - if (typeof response.callback === 'function') { - // Save async callback to be invoked after auction - submodule.callback = response.callback; - } - } - - if (storedId) { - // cache decoded value (this is copied to every adUnit bid) - submodule.idObj = submodule.submodule.decode(storedId, submodule.config.params); - } - } else if (submodule.config.value) { - // cache decoded value (this is copied to every adUnit bid) - submodule.idObj = submodule.config.value; - } else { - const response = submodule.submodule.getId(submodule.config.params, consentData, undefined); - if (utils.isPlainObject(response)) { - if (typeof response.callback === 'function') { submodule.callback = response.callback; } - if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config.params); } - } - } + populateSubmoduleId(submodule, consentData, storedConsentData, false); carry.push(submodule); return carry; }, []); @@ -660,6 +720,7 @@ export function init(config) { // exposing getUserIds function in global-name-space so that userIds stored in Prebid can be used by external codes. (getGlobal()).getUserIds = getUserIds; (getGlobal()).getUserIdsAsEids = getUserIdsAsEids; + (getGlobal()).refreshUserIds = refreshUserIds; } // init config update listener to start the application diff --git a/modules/userId/userId.md b/modules/userId/userId.md index a9ab6ccc483..c46f67f9a9a 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -70,6 +70,13 @@ pbjs.setConfig({ name: 'sharedid', expires: 28 } + }, { + name: 'criteo', + storage: { // It is best not to specify this parameter since the module needs to be called as many times as possible + type: 'cookie', + name: '_criteoId', + expires: 1 + } }], syncDelay: 5000, auctionDelay: 1000 @@ -141,6 +148,13 @@ pbjs.setConfig({ expires: 90, // Expiration of cookies in days refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires' }, + }, { + name: 'criteo', + storage: { // It is best not to specify this parameter since the module needs to be called as many times as possible + type: 'html5', + name: '_criteoId', + expires: 1 + } }], syncDelay: 5000 } @@ -164,6 +178,10 @@ pbjs.setConfig({ { name: "netId", value: { "netId": "fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg" } + }, + { + name: "criteo", + value: { "criteoId": "wK-fkF8zaEIlMkZMbHl3eFo4NEtoNmZaeXJtYkFjZlVuWjBhcjJMaTRYd3pZNSUyQnlKRHNGRXlpdzdjd3pjVzhjcSUyQmY4eTFzN3VSZjV1ZyUyRlA0U2ZiR0UwN2I4bDZRJTNEJTNE" } }], syncDelay: 5000 } diff --git a/modules/vuukleBidAdapter.js b/modules/vuukleBidAdapter.js new file mode 100644 index 00000000000..e9770b5e62e --- /dev/null +++ b/modules/vuukleBidAdapter.js @@ -0,0 +1,63 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'vuukle'; +const URL = 'https://pb.vuukle.com/adapter'; +const TIME_TO_LIVE = 360; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + return true + }, + + buildRequests: function(bidRequests) { + const requests = bidRequests.map(function (bid) { + const parseSized = utils.parseSizesInput(bid.sizes); + const arrSize = parseSized[0].split('x'); + const params = { + url: encodeURIComponent(window.location.href), + sizes: JSON.stringify(parseSized), + width: arrSize[0], + height: arrSize[1], + params: JSON.stringify(bid.params), + rnd: Math.random(), + bidId: bid.bidId, + source: 'pbjs', + version: '$prebid.version$', + v: 1, + }; + + return { + method: 'GET', + url: URL, + data: params, + options: {withCredentials: false} + } + }); + + return requests; + }, + + interpretResponse: function(serverResponse, bidRequest) { + if (!serverResponse || !serverResponse.body || !serverResponse.body.ad) { + return []; + } + + const res = serverResponse.body; + const bidResponse = { + requestId: bidRequest.data.bidId, + cpm: res.cpm, + width: res.width, + height: res.height, + creativeId: res.creative_id, + currency: res.currency || 'USD', + netRevenue: true, + ttl: TIME_TO_LIVE, + ad: res.ad + }; + return [bidResponse]; + }, +} +registerBidder(spec); diff --git a/modules/vuukleBidAdapter.md b/modules/vuukleBidAdapter.md new file mode 100644 index 00000000000..ee7b54c6262 --- /dev/null +++ b/modules/vuukleBidAdapter.md @@ -0,0 +1,26 @@ +# Overview +``` +Module Name: Vuukle Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@vuukle.com +``` + +# Description +Module that connects to Vuukle's server for bids. +Currently module supports only banner mediaType. + +# Test Parameters +``` + var adUnits = [{ + code: '/test/div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'vuukle', + params: {} + }] + }]; +``` diff --git a/modules/zeotapIdPlusIdSystem.js b/modules/zeotapIdPlusIdSystem.js index ea1173cd61e..d800286b00e 100644 --- a/modules/zeotapIdPlusIdSystem.js +++ b/modules/zeotapIdPlusIdSystem.js @@ -41,7 +41,7 @@ export const zeotapIdPlusSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} configParams + * @param {SubmoduleConfig} config * @return {{id: string | undefined} | undefined} */ getId() { diff --git a/package.json b/package.json index 35b0f6925e0..6273d680e19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.11.0-pre", + "version": "4.13.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { diff --git a/src/prebid.js b/src/prebid.js index 31e0140cfe2..8bfb6024d7a 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -149,7 +149,14 @@ export const checkAdUnitSetup = hook('sync', function (adUnits) { adUnits.forEach(adUnit => { const mediaTypes = adUnit.mediaTypes; + const bids = adUnit.bids; let validatedBanner, validatedVideo, validatedNative; + + if (!bids || !utils.isArray(bids)) { + utils.logError(`Detected adUnit.code '${adUnit.code}' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.`); + return; + } + if (!mediaTypes || Object.keys(mediaTypes).length === 0) { utils.logError(`Detected adUnit.code '${adUnit.code}' did not have a 'mediaTypes' object defined. This is a required field for the auction, so this adUnit has been removed.`); return; @@ -342,7 +349,7 @@ function emitAdRenderFail({ reason, message, bid, id }) { * @param {string} id bid id to locate the ad * @alias module:pbjs.renderAd */ -$$PREBID_GLOBAL$$.renderAd = function (doc, id) { +$$PREBID_GLOBAL$$.renderAd = function (doc, id, options) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.renderAd', arguments); utils.logMessage('Calling renderAd with adId :' + id); @@ -354,6 +361,14 @@ $$PREBID_GLOBAL$$.renderAd = function (doc, id) { // replace macros according to openRTB with price paid = bid.cpm bid.ad = utils.replaceAuctionPrice(bid.ad, bid.cpm); bid.adUrl = utils.replaceAuctionPrice(bid.adUrl, bid.cpm); + + // replacing clickthrough if submitted + if (options && options.clickThrough) { + const { clickThrough } = options; + bid.ad = utils.replaceClickThrough(bid.ad, clickThrough); + bid.adUrl = utils.replaceClickThrough(bid.adUrl, clickThrough); + } + // save winning bids auctionManager.addWinningBid(bid); diff --git a/src/utils.js b/src/utils.js index 9426308daf4..8af7a25668d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -718,6 +718,11 @@ export function replaceAuctionPrice(str, cpm) { return str.replace(/\$\{AUCTION_PRICE\}/g, cpm); } +export function replaceClickThrough(str, clicktag) { + if (!str || !clicktag || typeof clicktag !== 'string') return; + return str.replace(/\${CLICKTHROUGH}/g, clicktag); +} + export function timestamp() { return new Date().getTime(); } diff --git a/test/mocks/fabrickId.json b/test/mocks/fabrickId.json new file mode 100644 index 00000000000..a8723ec88ec --- /dev/null +++ b/test/mocks/fabrickId.json @@ -0,0 +1,3 @@ +{ + "fabrickId": 1980 +} diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index 9c694668703..62449771416 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from 'modules/adtelligentBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; +import { deepClone } from 'src/utils.js'; const EXPECTED_ENDPOINTS = [ 'https://ghb.adtelligent.com/v2/auction/', @@ -9,7 +10,9 @@ const EXPECTED_ENDPOINTS = [ 'https://ghb2.adtelligent.com/v2/auction/', 'https://ghb.adtelligent.com/v2/auction/' ]; - +const aliasEP = { + appaloosa: 'https://hb.appaloosa.media/v2/auction/' +}; const DISPLAY_REQUEST = { 'bidder': 'adtelligent', 'params': { @@ -250,6 +253,15 @@ describe('adtelligentBidAdapter', () => { expect(bidReqUrls).to.deep.equal(EXPECTED_ENDPOINTS); }) + it('makes correct host for aliases', () => { + for (const alias in aliasEP) { + const bidReq = deepClone(DISPLAY_REQUEST) + bidReq.bidder = alias; + const [bidderRequest] = spec.buildRequests([bidReq], { bidderCode: alias }); + expect(bidderRequest.url).to.equal(aliasEP[alias]); + } + }) + it('building requests as arrays', () => { expect(videoRequest).to.be.a('array'); expect(displayRequest).to.be.a('array'); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index d2d4e10c17f..ec8f2f00923 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -306,6 +306,7 @@ describe('Adyoulike Adapter', function () { expect(request.method).to.equal('POST'); expect(request.url).to.contains('CanonicalUrl=' + encodeURIComponent(canonicalUrl)); expect(request.url).to.contains('RefererUrl=' + encodeURIComponent(referrerUrl)); + expect(request.url).to.contains('PublisherDomain=http%3A%2F%2Flocalhost%3A9876'); expect(payload.Version).to.equal('1.0'); expect(payload.Bids['bid_id_0'].PlacementID).to.be.equal('placement_0'); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 7b563c4ec1d..4102896ba94 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -829,6 +829,52 @@ describe('AppNexusAdapter', function () { user_id: 'sample-criteo-userid', }); }); + + it('should populate iab_support object at the root level if omid support is detected', function () { + // with bid.params.frameworks + let bidRequest_A = Object.assign({}, bidRequests[0], { + params: { + frameworks: [1, 2, 5, 6], + video: { + frameworks: [1, 2, 5, 6] + } + } + }); + let request = spec.buildRequests([bidRequest_A]); + let payload = JSON.parse(request.data); + expect(payload.iab_support).to.be.an('object'); + expect(payload.iab_support).to.deep.equal({ + omidpn: 'Appnexus', + omidpv: '$prebid.version$' + }); + expect(payload.tags[0].banner_frameworks).to.be.an('array'); + expect(payload.tags[0].banner_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video_frameworks).to.be.an('array'); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video.frameworks).to.not.exist; + + // without bid.params.frameworks + const bidRequest_B = Object.assign({}, bidRequests[0]); + request = spec.buildRequests([bidRequest_B]); + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; + + // with video.frameworks but it is not an array + const bidRequest_C = Object.assign({}, bidRequests[0], { + params: { + video: { + frameworks: "'1', '2', '3', '6'" + } + } + }); + request = spec.buildRequests([bidRequest_C]); + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; + }); }) describe('interpretResponse', function () { diff --git a/test/spec/modules/atsAnalyticsAdapter_spec.js b/test/spec/modules/atsAnalyticsAdapter_spec.js index 849ccf88c51..84206337fad 100644 --- a/test/spec/modules/atsAnalyticsAdapter_spec.js +++ b/test/spec/modules/atsAnalyticsAdapter_spec.js @@ -18,7 +18,7 @@ describe('ats analytics adapter', function () { describe('track', function () { it('builds and sends request and response data', function () { - sinon.spy(atsAnalyticsAdapter, 'track'); + sinon.stub(atsAnalyticsAdapter, 'shouldFireRequest').returns(true); let initOptions = { pid: '10433394', @@ -134,7 +134,6 @@ describe('ats analytics adapter', function () { let requests = server.requests.filter(req => { return req.url.indexOf(initOptions.host) > -1; }); - expect(requests.length).to.equal(1); let realAfterBid = JSON.parse(requests[0].requestBody); diff --git a/test/spec/modules/britepoolIdSystem_spec.js b/test/spec/modules/britepoolIdSystem_spec.js index f2dd2ef533f..ddb61806006 100644 --- a/test/spec/modules/britepoolIdSystem_spec.js +++ b/test/spec/modules/britepoolIdSystem_spec.js @@ -28,17 +28,17 @@ describe('BritePool Submodule', () => { }); it('trigger id resolution pixel when no identifiers set', () => { - britepoolIdSubmodule.getId({}); + britepoolIdSubmodule.getId({ params: {} }); expect(triggerPixelStub.called).to.be.true; }); it('trigger id resolution pixel when no identifiers set with api_key param', () => { - britepoolIdSubmodule.getId({ api_key }); + britepoolIdSubmodule.getId({ params: { api_key } }); expect(triggerPixelStub.called).to.be.true; }); it('does not trigger id resolution pixel when identifiers set', () => { - britepoolIdSubmodule.getId({ api_key, aaid }); + britepoolIdSubmodule.getId({ params: { api_key, aaid } }); expect(triggerPixelStub.called).to.be.false; }); @@ -110,7 +110,7 @@ describe('BritePool Submodule', () => { expect(getter).to.equal(getter_override); // Making sure it did not become part of params expect(params.getter).to.be.undefined; - const response = britepoolIdSubmodule.getId({ api_key, aaid, url: url_override, getter: getter_override }); + const response = britepoolIdSubmodule.getId({ params: { api_key, aaid, url: url_override, getter: getter_override } }); assert.deepEqual(response, { id: { 'primaryBPID': bpid } }); }); @@ -119,7 +119,7 @@ describe('BritePool Submodule', () => { expect(getter).to.equal(getter_callback_override); // Making sure it did not become part of params expect(params.getter).to.be.undefined; - const response = britepoolIdSubmodule.getId({ api_key, aaid, url: url_override, getter: getter_callback_override }); + const response = britepoolIdSubmodule.getId({ params: { api_key, aaid, url: url_override, getter: getter_callback_override } }); expect(response.callback).to.not.be.undefined; response.callback(result => { assert.deepEqual(result, { 'primaryBPID': bpid }); diff --git a/test/spec/modules/fabrickIdSystem_spec.js b/test/spec/modules/fabrickIdSystem_spec.js new file mode 100644 index 00000000000..cbd538816ab --- /dev/null +++ b/test/spec/modules/fabrickIdSystem_spec.js @@ -0,0 +1,106 @@ +import * as utils from '../../../src/utils.js'; +import {server} from '../../mocks/xhr.js'; + +import * as fabrickIdSystem from 'modules/fabrickIdSystem.js'; + +const defaultConfigParams = { + apiKey: '123', + e: 'abc', + p: ['def', 'hij'], + url: 'http://localhost:9999/test/mocks/fabrickId.json?' +}; +const responseHeader = {'Content-Type': 'application/json'} +const fabrickIdSubmodule = fabrickIdSystem.fabrickIdSubmodule; + +describe('Fabrick ID System', function() { + let logErrorStub; + + beforeEach(function () { + logErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + logErrorStub.restore(); + fabrickIdSubmodule.getRefererInfoOverride = null; + }); + + it('should log an error if no configParams were passed into getId', function () { + fabrickIdSubmodule.getId(); + expect(logErrorStub.calledOnce).to.be.true; + }); + + it('should error on json parsing', function() { + let submoduleCallback = fabrickIdSubmodule.getId({ + name: 'fabrickId', + params: defaultConfigParams + }).callback; + let callBackSpy = sinon.spy(); + submoduleCallback(callBackSpy); + let request = server.requests[0]; + request.respond( + 200, + responseHeader, + '] this is not json {' + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(logErrorStub.calledOnce).to.be.true; + }); + + it('should truncate the params', function() { + let r = ''; + for (let i = 0; i < 300; i++) { + r += 'r'; + } + let configParams = Object.assign({}, defaultConfigParams, { + refererInfo: { + referer: r, + stack: ['s-0'], + canonicalUrl: 'cu-0' + } + }); + let submoduleCallback = fabrickIdSubmodule.getId({ + name: 'fabrickId', + params: configParams + }).callback; + let callBackSpy = sinon.spy(); + submoduleCallback(callBackSpy); + let request = server.requests[0]; + r = ''; + for (let i = 0; i < 200; i++) { + r += 'r'; + } + expect(request.url).to.match(new RegExp(`r=${r}&r=`)); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(logErrorStub.calledOnce).to.be.false; + }); + + it('should complete successfully', function() { + let configParams = Object.assign({}, defaultConfigParams, { + refererInfo: { + referer: 'r-0', + stack: ['s-0'], + canonicalUrl: 'cu-0' + } + }); + let submoduleCallback = fabrickIdSubmodule.getId({ + name: 'fabrickId', + params: configParams + }).callback; + let callBackSpy = sinon.spy(); + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.match(/r=r-0&r=s-0&r=cu-0&r=http/); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(logErrorStub.calledOnce).to.be.false; + }); +}); diff --git a/test/spec/modules/hybridBidAdapter_spec.js b/test/spec/modules/hybridBidAdapter_spec.js index e5ad878c9b1..d1899ef3d81 100644 --- a/test/spec/modules/hybridBidAdapter_spec.js +++ b/test/spec/modules/hybridBidAdapter_spec.js @@ -25,16 +25,22 @@ describe('Hybrid.ai Adapter', function() { placeId: PLACE_ID, placement: 'video' } + const inImageMandatoryParams = { + placeId: PLACE_ID, + placement: 'inImage', + imageUrl: 'https://hybrid.ai/images/image.jpg' + } const validBidRequests = [ getSlotConfigs({ banner: {} }, bannerMandatoryParams), - getSlotConfigs({ video: {playerSize: [[640, 480]]} }, videoMandatoryParams) + getSlotConfigs({ video: {playerSize: [[640, 480]], context: 'outstream'} }, videoMandatoryParams), + getSlotConfigs({ banner: {sizes: [0, 0]} }, inImageMandatoryParams) ] describe('isBidRequestValid method', function() { describe('returns true', function() { describe('when banner slot config has all mandatory params', () => { - describe('and placement has the correct value', function() { + describe('and banner placement has the correct value', function() { const slotConfig = getSlotConfigs( - { banner: {} }, + {banner: {}}, { placeId: PLACE_ID, placement: 'banner' @@ -43,6 +49,22 @@ describe('Hybrid.ai Adapter', function() { const isBidRequestValid = spec.isBidRequestValid(slotConfig) expect(isBidRequestValid).to.equal(true) }) + describe('and In-Image placement has the correct value', function() { + const slotConfig = getSlotConfigs( + { + banner: { + sizes: [[0, 0]] + } + }, + { + placeId: PLACE_ID, + placement: 'inImage', + imageUrl: 'imageUrl' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) describe('when video slot has all mandatory params.', function() { it('should return true, when video mediatype object are correct.', function() { const slotConfig = getSlotConfigs( @@ -84,6 +106,15 @@ describe('Hybrid.ai Adapter', function() { ) expect(isBidRequestValid).to.equal(false) }) + it('does not have the imageUrl.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placeId: PLACE_ID, + placement: 'inImage' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) it('does not have a the correct placement.', function() { const isBidRequestValid = spec.isBidRequestValid( createSlotconfig({ @@ -240,6 +271,36 @@ describe('Hybrid.ai Adapter', function() { expect(bids[0].netRevenue).to.equal(true) expect(typeof bids[0].ad).to.equal('string') }) + it('should return a In-Image bid', function() { + const request = spec.buildRequests([validBidRequests[2]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: 'html', + inImage: { + actionUrls: {} + }, + width: 100, + height: 100, + ttl: 360 + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].width).to.equal(100) + expect(bids[0].height).to.equal(100) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].ad).to.equal('string') + }) }) describe('the bid is a video', function() { it('should return a video bid', function() { @@ -253,7 +314,8 @@ describe('Hybrid.ai Adapter', function() { currency: 'USD', content: 'html', width: 100, - height: 100 + height: 100, + transactionId: '31a58515-3634-4e90-9c96-f86196db1459' } ] } diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index cea6bdf92b9..ac000c1e6dd 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -95,10 +95,12 @@ 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().params, undefined, ID5_LEGACY_STORED_OBJ).callback; + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_LEGACY_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; @@ -115,7 +117,7 @@ describe('ID5 ID System', function() { }); it('should call the ID5 server with signature field for new storedObj format', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).callback; + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; @@ -134,8 +136,8 @@ describe('ID5 ID System', function() { it('should call the ID5 server with pd field when pd config is set', function () { const pubData = 'b50ca08271795a8e7e4012813f23d505193d75c0f2e2bb99baa63aa822f66ed3'; - let config = getId5FetchConfig().params; - config.pd = pubData; + let config = getId5FetchConfig(); + config.params.pd = pubData; let submoduleCallback = id5IdSubmodule.getId(config, undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); @@ -154,8 +156,8 @@ describe('ID5 ID System', function() { }); it('should call the ID5 server with empty pd field when pd config is not set', function () { - let config = getId5FetchConfig().params; - config.pd = undefined; + let config = getId5FetchConfig(); + config.params.pd = undefined; let submoduleCallback = id5IdSubmodule.getId(config, undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); @@ -174,7 +176,7 @@ describe('ID5 ID System', function() { it('should call the ID5 server with nb=1 when no stored value exists', function () { coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).callback; + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; @@ -194,7 +196,7 @@ describe('ID5 ID System', function() { let expStr = (new Date(Date.now() + 25000).toUTCString()); coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).callback; + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index 9f36ba92558..3c544fa8d08 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -1,9 +1,12 @@ import {identityLinkSubmodule} from 'modules/identityLinkIdSystem.js'; import * as utils from 'src/utils.js'; import {server} from 'test/mocks/xhr.js'; +import {getStorageManager} from '../../../src/storageManager.js'; + +export const storage = getStorageManager(); const pid = '14'; -const defaultConfigParams = {pid: pid}; +const defaultConfigParams = { params: {pid: pid} }; const responseHeader = {'Content-Type': 'application/json'} describe('IdentityLinkId tests', function () { @@ -11,6 +14,8 @@ describe('IdentityLinkId tests', function () { beforeEach(function () { logErrorStub = sinon.stub(utils, 'logError'); + // remove _lr_retry_request cookie before test + storage.setCookie('_lr_retry_request', 'true', 'Thu, 01 Jan 1970 00:00:01 GMT'); }); afterEach(function () { @@ -18,12 +23,12 @@ describe('IdentityLinkId tests', function () { }); it('should log an error if no configParams were passed when getId', function () { - identityLinkSubmodule.getId(); + identityLinkSubmodule.getId({ params: {} }); expect(logErrorStub.calledOnce).to.be.true; }); it('should log an error if pid configParam was not passed when getId', function () { - identityLinkSubmodule.getId({}); + identityLinkSubmodule.getId({ params: {} }); expect(logErrorStub.calledOnce).to.be.true; }); @@ -124,4 +129,29 @@ describe('IdentityLinkId tests', function () { ); expect(callBackSpy.calledOnce).to.be.true; }); + + it('should not call the LiveRamp envelope endpoint if cookie _lr_retry_request exist', function () { + let now = new Date(); + now.setTime(now.getTime() + 3000); + storage.setCookie('_lr_retry_request', 'true', now.toUTCString()); + let callBackSpy = sinon.spy(); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request).to.be.eq(undefined); + }); + + it('should call the LiveRamp envelope endpoint if cookie _lr_retry_request does not exist', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); }); diff --git a/test/spec/modules/idxIdSystem_spec.js b/test/spec/modules/idxIdSystem_spec.js new file mode 100644 index 00000000000..14cd9a88d13 --- /dev/null +++ b/test/spec/modules/idxIdSystem_spec.js @@ -0,0 +1,117 @@ +import { expect } from 'chai'; +import find from 'core-js-pure/features/array/find.js'; +import { config } from 'src/config.js'; +import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; +import { storage, idxIdSubmodule } from 'modules/idxIdSystem.js'; + +const IDX_COOKIE_NAME = '_idx'; +const IDX_DUMMY_VALUE = 'idx value for testing'; +const IDX_COOKIE_STORED = '{ "idx": "' + IDX_DUMMY_VALUE + '" }'; +const ID_COOKIE_OBJECT = { id: IDX_DUMMY_VALUE }; +const IDX_COOKIE_OBJECT = { idx: IDX_DUMMY_VALUE }; + +function getConfigMock() { + return { + userSync: { + syncDelay: 0, + userIds: [{ + name: 'idx' + }] + } + } +} + +function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: {banner: {}, native: {}}, + sizes: [ + [300, 200], + [300, 600] + ], + bids: [{ + bidder: 'sampleBidder', + params: { placementId: 'banner-only-bidder' } + }] + }; +} + +describe('IDx ID System', () => { + let getDataFromLocalStorageStub, localStorageIsEnabledStub; + let getCookieStub, cookiesAreEnabledStub; + + beforeEach(() => { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + getCookieStub = sinon.stub(storage, 'getCookie'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + }); + + afterEach(() => { + getDataFromLocalStorageStub.restore(); + localStorageIsEnabledStub.restore(); + getCookieStub.restore(); + cookiesAreEnabledStub.restore(); + }); + + describe('IDx: test "getId" method', () => { + it('provides the stored IDx if a cookie exists', () => { + getCookieStub.withArgs(IDX_COOKIE_NAME).returns(IDX_COOKIE_STORED); + let idx = idxIdSubmodule.getId(); + expect(idx).to.deep.equal(ID_COOKIE_OBJECT); + }); + + it('provides the stored IDx if cookie is absent but present in local storage', () => { + getDataFromLocalStorageStub.withArgs(IDX_COOKIE_NAME).returns(IDX_COOKIE_STORED); + let idx = idxIdSubmodule.getId(); + expect(idx).to.deep.equal(ID_COOKIE_OBJECT); + }); + + it('returns undefined if both cookie and local storage are empty', () => { + let idx = idxIdSubmodule.getId(); + expect(idx).to.be.undefined; + }) + }); + + describe('IDx: test "decode" method', () => { + it('provides the IDx from a stored object', () => { + expect(idxIdSubmodule.decode(ID_COOKIE_OBJECT)).to.deep.equal(IDX_COOKIE_OBJECT); + }); + + it('provides the IDx from a stored string', () => { + expect(idxIdSubmodule.decode(IDX_DUMMY_VALUE)).to.deep.equal(IDX_COOKIE_OBJECT); + }); + }); + + describe('requestBids hook', () => { + let adUnits; + + beforeEach(() => { + adUnits = [getAdUnitMock()]; + setSubmoduleRegistry([idxIdSubmodule]); + init(config); + config.setConfig(getConfigMock()); + getCookieStub.withArgs(IDX_COOKIE_NAME).returns(IDX_COOKIE_STORED); + }); + + it('when a stored IDx exists it is added to bids', (done) => { + requestBidsHook(() => { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.idx'); + expect(bid.userId.idx).to.equal(IDX_DUMMY_VALUE); + const idxIdAsEid = find(bid.userIdAsEids, e => e.source == 'idx.lat'); + expect(idxIdAsEid).to.deep.equal({ + source: 'idx.lat', + uids: [{ + id: IDX_DUMMY_VALUE, + atype: 1, + }] + }); + }); + }); + done(); + }, { adUnits }); + }); + }); +}); diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index 951cfab5f50..4b45342cb6f 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -6,10 +6,10 @@ import {server} from 'test/mocks/xhr.js'; const partner = 10; const pai = '11'; const pcid = '12'; -const defaultConfigParams = {partner: partner}; -const paiConfigParams = {partner: partner, pai: pai}; -const pcidConfigParams = {partner: partner, pcid: pcid}; -const allConfigParams = {partner: partner, pai: pai, pcid: pcid}; +const defaultConfigParams = { params: {partner: partner} }; +const paiConfigParams = { params: {partner: partner, pai: pai} }; +const pcidConfigParams = { params: {partner: partner, pcid: pcid} }; +const allConfigParams = { params: {partner: partner, pai: pai, pcid: pcid} }; const responseHeader = {'Content-Type': 'application/json'} describe('IntentIQ tests', function () { @@ -24,19 +24,19 @@ describe('IntentIQ tests', function () { }); it('should log an error if no configParams were passed when getId', function () { - let submodule = intentIqIdSubmodule.getId(); + let submodule = intentIqIdSubmodule.getId({ params: {} }); expect(logErrorStub.calledOnce).to.be.true; expect(submodule).to.be.undefined; }); it('should log an error if partner configParam was not passed when getId', function () { - let submodule = intentIqIdSubmodule.getId({}); + let submodule = intentIqIdSubmodule.getId({ params: {} }); expect(logErrorStub.calledOnce).to.be.true; expect(submodule).to.be.undefined; }); it('should log an error if partner configParam was not a numeric value', function () { - let submodule = intentIqIdSubmodule.getId({partner: '10'}); + let submodule = intentIqIdSubmodule.getId({ params: {partner: '10'} }); expect(logErrorStub.calledOnce).to.be.true; expect(submodule).to.be.undefined; }); diff --git a/test/spec/modules/ironsourceBidAdapter_spec.js b/test/spec/modules/ironsourceBidAdapter_spec.js index cfdc51e0235..0c59dfef14b 100644 --- a/test/spec/modules/ironsourceBidAdapter_spec.js +++ b/test/spec/modules/ironsourceBidAdapter_spec.js @@ -5,6 +5,7 @@ import { config } from 'src/config.js'; import { VIDEO } from '../../../src/mediaTypes.js'; const ENDPOINT = 'https://hb.yellowblue.io/hb'; +const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-test'; const TTL = 360; describe('ironsourceAdapter', function () { @@ -55,6 +56,21 @@ describe('ironsourceAdapter', function () { } ]; + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'isOrg': 'jdye8weeyirk00000001', + 'testMode': true + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + const bidderRequest = { bidderCode: 'ironsource', } @@ -67,6 +83,14 @@ describe('ironsourceAdapter', function () { } }); + it('sends bid request to test ENDPOINT via GET', function () { + const requests = spec.buildRequests(testModeBidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + it('should send the correct bid Id', function () { const requests = spec.buildRequests(bidRequests, bidderRequest); for (const request of requests) { diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index b19d38d5859..80f776168c4 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -4,7 +4,7 @@ import {uspDataHandler} from '../../../src/adapterManager.js'; import {server} from 'test/mocks/xhr.js'; const PUBLISHER_ID = '89899'; -const defaultConfigParams = {publisherId: PUBLISHER_ID}; +const defaultConfigParams = { params: {publisherId: PUBLISHER_ID} }; const responseHeader = {'Content-Type': 'application/json'} describe('LiveIntentId', function () { @@ -55,8 +55,8 @@ describe('LiveIntentId', function () { }); it('should initialize LiveConnect with the config params when decode and emit an event', function () { - liveIntentIdSubmodule.decode({}, { - ...defaultConfigParams, + liveIntentIdSubmodule.decode({}, { params: { + ...defaultConfigParams.params, ...{ url: 'https://dummy.liveintent.com', liCollectConfig: { @@ -64,7 +64,7 @@ describe('LiveIntentId', function () { collectorUrl: 'https://collector.liveintent.com' } } - }); + } }); expect(pixel.src).to.match(/https:\/\/collector.liveintent.com\/p\?aid=a-0001&wpn=prebid.*/) }); @@ -95,7 +95,7 @@ describe('LiveIntentId', 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({...defaultConfigParams, ...{'url': 'https://dummy.liveintent.com/idex'}}).callback; + let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams.params, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899'); @@ -110,13 +110,13 @@ describe('LiveIntentId', 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({ - ...defaultConfigParams, + let submoduleCallback = liveIntentIdSubmodule.getId({ params: { + ...defaultConfigParams.params, ...{ 'url': 'https://dummy.liveintent.com/idex', 'partner': 'rubicon' } - }).callback; + } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899'); @@ -179,12 +179,12 @@ describe('LiveIntentId', function () { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); getDataFromLocalStorageStub.withArgs('_thirdPC').returns('third-pc'); - const configParams = { - ...defaultConfigParams, + const configParams = { params: { + ...defaultConfigParams.params, ...{ 'identifiersToResolve': ['_thirdPC'] } - }; + }}; let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); @@ -201,12 +201,12 @@ describe('LiveIntentId', 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 = { - ...defaultConfigParams, + const configParams = { params: { + ...defaultConfigParams.params, ...{ 'identifiersToResolve': ['_thirdPC'] } - }; + }}; let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); diff --git a/test/spec/modules/mediaforceBidAdapter_spec.js b/test/spec/modules/mediaforceBidAdapter_spec.js index ee478acbc83..0b3271da770 100644 --- a/test/spec/modules/mediaforceBidAdapter_spec.js +++ b/test/spec/modules/mediaforceBidAdapter_spec.js @@ -100,6 +100,37 @@ describe('mediaforce bid adapter', function () { transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', }; + const multiBid = [ + { + publisher_id: 'pub123', + placement_id: '202', + }, + { + publisher_id: 'pub123', + placement_id: '203', + }, + { + publisher_id: 'pub124', + placement_id: '202', + }, + { + publisher_id: 'pub123', + placement_id: '203', + transactionId: '8df76688-1618-417a-87b1-60ad046841c9' + } + ].map(({publisher_id, placement_id, transactionId}) => { + return { + bidder: 'mediaforce', + params: {publisher_id, placement_id}, + mediaTypes: { + banner: { + sizes: [[300, 250], [600, 400]] + } + }, + transactionId: transactionId || 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }); + const refererInfo = { referer: 'https://www.prebid.org', reachedTop: true, @@ -111,7 +142,9 @@ describe('mediaforce bid adapter', function () { const requestUrl = `${baseUrl}/header_bid`; const dnt = utils.getDNT() ? 1 : 0; - const secure = 1; + const secure = window.location.protocol === 'https' ? 1 : 0; + const pageUrl = window.location.href; + const timeout = 1500; it('should return undefined if no validBidRequests passed', function () { assert.equal(spec.buildRequests([]), undefined); @@ -127,18 +160,29 @@ describe('mediaforce bid adapter', function () { bid.params.bidfloor = 0.5; let bidRequests = [bid]; - let bidderRequest = {bids: bidRequests, refererInfo: refererInfo}; + let bidderRequest = { + bids: bidRequests, + refererInfo: refererInfo, + timeout: timeout, + auctionId: '210a474e-88f0-4646-837f-4253b7cf14fb' + }; let [request] = spec.buildRequests(bidRequests, bidderRequest); let data = JSON.parse(request.data); assert.deepEqual(data, { - id: bid.transactionId, + id: data.id, + tmax: timeout, + ext: { + mediaforce: { + hb_key: bidderRequest.auctionId + } + }, site: { id: bid.params.publisher_id, publisher: {id: bid.params.publisher_id}, ref: encodeURIComponent(refererInfo.referer), - page: encodeURIComponent(refererInfo.referer), + page: pageUrl, }, device: { ua: navigator.userAgent, @@ -150,6 +194,11 @@ describe('mediaforce bid adapter', function () { tagid: bid.params.placement_id, secure: secure, bidfloor: bid.params.bidfloor, + ext: { + mediaforce: { + transactionId: bid.transactionId + } + }, banner: {w: 300, h: 250}, native: { ver: '1.2', @@ -170,7 +219,7 @@ describe('mediaforce bid adapter', function () { assert.deepEqual(request, { method: 'POST', url: requestUrl, - data: '{"id":"d45dd707-a418-42ec-b8a7-b70a6c6fab0b","site":{"page":"https%3A%2F%2Fwww.prebid.org","ref":"https%3A%2F%2Fwww.prebid.org","id":"pub123","publisher":{"id":"pub123"}},"device":{"ua":"' + navigator.userAgent + '","js":1,"dnt":' + dnt + ',"language":"' + language + '"},"imp":[{"tagid":"202","secure":1,"bidfloor":0.5,"banner":{"w":300,"h":250},"native":{"ver":"1.2","request":{"assets":[{"required":1,"id":1,"title":{"len":800}},{"required":1,"id":3,"img":{"type":3,"w":300,"h":250}},{"required":1,"id":5,"data":{"type":1}}],"context":1,"plcmttype":1,"ver":"1.2"}}}]}', + data: '{"id":"' + data.id + '","site":{"page":"' + pageUrl + '","ref":"https%3A%2F%2Fwww.prebid.org","id":"pub123","publisher":{"id":"pub123"}},"device":{"ua":"' + navigator.userAgent + '","js":1,"dnt":' + dnt + ',"language":"' + language + '"},"ext":{"mediaforce":{"hb_key":"210a474e-88f0-4646-837f-4253b7cf14fb"}},"tmax":1500,"imp":[{"tagid":"202","secure":' + secure + ',"bidfloor":0.5,"ext":{"mediaforce":{"transactionId":"d45dd707-a418-42ec-b8a7-b70a6c6fab0b"}},"banner":{"w":300,"h":250},"native":{"ver":"1.2","request":{"assets":[{"required":1,"id":1,"title":{"len":800}},{"required":1,"id":3,"img":{"type":3,"w":300,"h":250}},{"required":1,"id":5,"data":{"type":1}}],"context":1,"plcmttype":1,"ver":"1.2"}}}]}', }); }); @@ -186,6 +235,116 @@ describe('mediaforce bid adapter', function () { let data = JSON.parse(request.data); assert.deepEqual(data.imp[0].banner, {w: 300, h: 600, format: [{w: 300, h: 250}]}); }); + + it('should return proper requests for multiple imps', function () { + let bidderRequest = { + bids: multiBid, + refererInfo: refererInfo, + timeout: timeout, + auctionId: '210a474e-88f0-4646-837f-4253b7cf14fb' + }; + + let requests = spec.buildRequests(multiBid, bidderRequest); + assert.equal(requests.length, 2); + requests.forEach((req) => { + req.data = JSON.parse(req.data); + }); + + assert.deepEqual(requests, [ + { + method: 'POST', + url: requestUrl, + data: { + id: requests[0].data.id, + tmax: timeout, + ext: { + mediaforce: { + hb_key: bidderRequest.auctionId + } + }, + site: { + id: 'pub123', + publisher: {id: 'pub123'}, + ref: encodeURIComponent(refererInfo.referer), + page: pageUrl, + }, + device: { + ua: navigator.userAgent, + dnt: dnt, + js: 1, + language: language, + }, + imp: [{ + tagid: '202', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }, { + tagid: '203', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }, { + tagid: '203', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: '8df76688-1618-417a-87b1-60ad046841c9' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }] + } + }, + { + method: 'POST', + url: requestUrl, + data: { + id: requests[1].data.id, + tmax: timeout, + ext: { + mediaforce: { + hb_key: bidderRequest.auctionId + } + }, + site: { + id: 'pub124', + publisher: {id: 'pub124'}, + ref: encodeURIComponent(refererInfo.referer), + page: pageUrl, + }, + device: { + ua: navigator.userAgent, + dnt: dnt, + js: 1, + language: language, + }, + imp: [{ + tagid: '202', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }] + } + } + ]); + }); }); describe('interpretResponse() banner', function () { diff --git a/test/spec/modules/ooloAnalyticsAdapter_spec.js b/test/spec/modules/ooloAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..f82f7856fb2 --- /dev/null +++ b/test/spec/modules/ooloAnalyticsAdapter_spec.js @@ -0,0 +1,793 @@ +import ooloAnalytics, { PAGEVIEW_ID } from 'modules/ooloAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +import constants from 'src/constants.json' +import events from 'src/events' +import { config } from 'src/config'; +import { buildAuctionData, generatePageViewId } from 'modules/ooloAnalyticsAdapter'; + +const auctionId = '0ea14159-2058-4b87-a966-9d7652176a56'; +const auctionStart = 1598513385415 +const timeout = 3000 +const adUnit1 = 'top_1'; +const adUnit2 = 'top_2'; +const bidId1 = '392b5a6b05d648' +const bidId2 = '392b5a6b05d649' +const bidId3 = '392b5a6b05d650' + +const auctionInit = { + timestamp: auctionStart, + auctionId: auctionId, + timeout: timeout, + auctionStart, + adUnits: [{ + code: adUnit1, + transactionId: 'abalksdkjfh-12sade' + }, { + code: adUnit2, + transactionId: 'abalksdkjfh-12sadf' + }] +}; + +const bidRequested = { + auctionId, + bidderCode: 'appnexus', + bidderRequestId: '2946b569352ef2', + start: 1598513405254, + bids: [ + { + auctionId, + bidId: bidId1, + bidderRequestId: '2946b569352ef2', + bidder: 'appnexus', + adUnitCode: adUnit1, + sizes: [[728, 90], [970, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90], [970, 90]], + }, + }, + params: { + placementId: '4799418', + }, + schain: {} + }, + { + auctionId, + bidId: bidId2, + bidderRequestId: '2946b569352ef3', + bidder: 'rubicon', + adUnitCode: adUnit2, + sizes: [[728, 90], [970, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90], [970, 90]], + }, + }, + params: { + placementId: '4799418', + }, + schain: {} + }, + { + auctionId, + bidId: bidId3, + bidderRequestId: '2946b569352ef4', + bidder: 'ix', + adUnitCode: adUnit2, + sizes: [[728, 90], [970, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90], [970, 90]], + }, + }, + params: { + placementId: '4799418', + }, + schain: {} + }, + ], +} + +const noBid = { + auctionId, + adUnitCode: adUnit2, + bidId: bidId2, + requestId: bidId2 +} + +const bidResponse = { + auctionId, + bidderCode: 'appnexus', + mediaType: 'banner', + width: 0, + height: 0, + statusMessage: 'Bid available', + adId: '222bb26f9e8bd', + cpm: 0.112256, + responseTimestamp: 1598513485254, + requestTimestamp: 1462919238936, + bidder: 'appnexus', + adUnitCode: adUnit1, + timeToRespond: 401, + pbLg: '0.00', + pbMg: '0.10', + pbHg: '0.11', + pbAg: '0.10', + size: '0x0', + requestId: bidId1, + creativeId: '123456', + adserverTargeting: { + hb_bidder: 'appnexus', + hb_adid: '222bb26f9e8bd', + hb_pb: '10.00', + hb_size: '0x0', + foobar: '0x0', + }, + netRevenue: true, + currency: 'USD', + ttl: 300, +} + +const auctionEnd = { + auctionId: auctionId +}; + +const bidTimeout = [ + { + adUnitCode: adUnit2, + auctionId: auctionId, + bidId: bidId3, + bidder: 'ix', + timeout: timeout + } +]; + +const bidWon = { + auctionId, + adUnitCode: adUnit1, + bidId: bidId1, + cpm: 0.5 +} + +function simulateAuction () { + events.emit(constants.EVENTS.AUCTION_INIT, auctionInit); + events.emit(constants.EVENTS.BID_REQUESTED, bidRequested); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponse); + events.emit(constants.EVENTS.NO_BID, noBid); + events.emit(constants.EVENTS.BID_TIMEOUT, bidTimeout); + events.emit(constants.EVENTS.AUCTION_END, auctionEnd); +} + +describe('oolo Prebid Analytic', () => { + let clock + + beforeEach(() => { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers() + }); + + afterEach(() => { + ooloAnalytics.disableAnalytics(); + events.getEvents.restore() + clock.restore(); + }) + + describe('enableAnalytics init options', () => { + it('should not enable analytics if invalid config', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: undefined + } + }) + + expect(server.requests).to.have.length(0) + }) + + it('should send prebid config to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + const conf = {} + const pbjsConfig = config.getConfig() + + Object.keys(pbjsConfig).forEach(key => { + if (key[0] !== '_') { + conf[key] = pbjsConfig[key] + } + }) + + expect(server.requests[1].url).to.contain('/hbconf') + expect(JSON.parse(server.requests[1].requestBody)).to.deep.equal(conf) + }) + + it('should request server config and send page data', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + expect(server.requests[0].url).to.contain('?pid=123') + + const pageData = JSON.parse(server.requests[2].requestBody) + + expect(pageData).to.have.property('timestamp') + expect(pageData).to.have.property('screenWidth') + expect(pageData).to.have.property('screenHeight') + expect(pageData).to.have.property('url') + expect(pageData).to.have.property('protocol') + expect(pageData).to.have.property('origin') + expect(pageData).to.have.property('referrer') + expect(pageData).to.have.property('pbVersion') + expect(pageData).to.have.property('pvid') + expect(pageData).to.have.property('pid') + expect(pageData).to.have.property('pbModuleVersion') + expect(pageData).to.have.property('domContentLoadTime') + expect(pageData).to.have.property('pageLoadTime') + }) + }) + + describe('data handling and sending events', () => { + it('should send an "auction" event to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(500) + + simulateAuction() + + expect(server.requests.length).to.equal(3) + clock.tick(2000) + expect(server.requests.length).to.equal(4) + + const request = JSON.parse(server.requests[3].requestBody); + + expect(request).to.include({ + eventType: 'auction', + pid: 123, + auctionId, + auctionStart, + auctionEnd: 0, + timeout, + }) + expect(request.pvid).to.be.a('number') + expect(request.adUnits).to.have.length(2) + + const auctionAdUnit1 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit1)[0] + const auctionAdUnit2 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit2)[0] + + expect(auctionAdUnit1.auctionId).to.equal(auctionId) + expect(auctionAdUnit1.bids).to.be.an('array') + expect(auctionAdUnit1.bids).to.have.length(1) + + // bid response + expect(auctionAdUnit1.bids[0].bidId).to.equal(bidId1) + expect(auctionAdUnit1.bids[0].bst).to.equal('bidReceived') + expect(auctionAdUnit1.bids[0].bidder).to.equal('appnexus') + expect(auctionAdUnit1.bids[0].cpm).to.equal(0.112256) + expect(auctionAdUnit1.bids[0].cur).to.equal('USD') + expect(auctionAdUnit1.bids[0].s).to.equal(bidRequested.start) + expect(auctionAdUnit1.bids[0].e).to.equal(bidResponse.responseTimestamp) + expect(auctionAdUnit1.bids[0].rs).to.equal(bidRequested.start - auctionStart) + expect(auctionAdUnit1.bids[0].re).to.equal(bidResponse.responseTimestamp - auctionStart) + expect(auctionAdUnit1.bids[0].h).to.equal(0) + expect(auctionAdUnit1.bids[0].w).to.equal(0) + expect(auctionAdUnit1.bids[0].mt).to.equal('banner') + expect(auctionAdUnit1.bids[0].nrv).to.equal(true) + expect(auctionAdUnit1.bids[0].params).to.have.keys('placementId') + expect(auctionAdUnit1.bids[0].size).to.equal('0x0') + expect(auctionAdUnit1.bids[0].crId).to.equal('123456') + expect(auctionAdUnit1.bids[0].ttl).to.equal(bidResponse.ttl) + expect(auctionAdUnit1.bids[0].ttr).to.equal(bidResponse.timeToRespond) + + expect(auctionAdUnit2.auctionId).to.equal(auctionId) + expect(auctionAdUnit2.bids).to.be.an('array') + expect(auctionAdUnit2.bids).to.have.length(2) + + // no bid + expect(auctionAdUnit2.bids[0].bidId).to.equal(bidId2) + expect(auctionAdUnit2.bids[0].bst).to.equal('noBid') + expect(auctionAdUnit2.bids[0].bidder).to.equal('rubicon') + expect(auctionAdUnit2.bids[0].s).to.be.a('number') + expect(auctionAdUnit2.bids[0].e).to.be.a('number') + expect(auctionAdUnit2.bids[0].params).to.have.keys('placementId') + + // timeout + expect(auctionAdUnit2.bids[1].bidId).to.equal(bidId3) + expect(auctionAdUnit2.bids[1].bst).to.equal('bidTimedOut') + expect(auctionAdUnit2.bids[1].bidder).to.equal('ix') + expect(auctionAdUnit2.bids[1].s).to.be.a('number') + expect(auctionAdUnit2.bids[1].e).to.be.a('undefined') + }) + + it('should push events to a queue and process them once server configuration returns', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + events.emit(constants.EVENTS.AUCTION_INIT, auctionInit); + events.emit(constants.EVENTS.BID_REQUESTED, bidRequested); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponse); + + // configuration returned in an arbitrary moment + server.requests[0].respond(500) + + events.emit(constants.EVENTS.NO_BID, noBid); + events.emit(constants.EVENTS.BID_TIMEOUT, bidTimeout); + events.emit(constants.EVENTS.AUCTION_END, auctionEnd); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody); + + expect(request).to.include({ + eventType: 'auction', + pid: 123, + auctionId, + auctionStart, + auctionEnd: 0, + timeout, + }) + expect(request.pvid).to.be.a('number') + expect(request.adUnits).to.have.length(2) + + const auctionAdUnit1 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit1)[0] + const auctionAdUnit2 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit2)[0] + + expect(auctionAdUnit1.auctionId).to.equal(auctionId) + expect(auctionAdUnit1.bids).to.be.an('array') + expect(auctionAdUnit1.bids).to.have.length(1) + + // bid response + expect(auctionAdUnit1.bids[0].bidId).to.equal(bidId1) + expect(auctionAdUnit1.bids[0].bst).to.equal('bidReceived') + expect(auctionAdUnit1.bids[0].bidder).to.equal('appnexus') + expect(auctionAdUnit1.bids[0].cpm).to.equal(0.112256) + expect(auctionAdUnit1.bids[0].cur).to.equal('USD') + expect(auctionAdUnit1.bids[0].s).to.equal(bidRequested.start) + expect(auctionAdUnit1.bids[0].e).to.equal(bidResponse.responseTimestamp) + expect(auctionAdUnit1.bids[0].rs).to.equal(bidRequested.start - auctionStart) + expect(auctionAdUnit1.bids[0].re).to.equal(bidResponse.responseTimestamp - auctionStart) + expect(auctionAdUnit1.bids[0].h).to.equal(0) + expect(auctionAdUnit1.bids[0].w).to.equal(0) + expect(auctionAdUnit1.bids[0].mt).to.equal('banner') + expect(auctionAdUnit1.bids[0].nrv).to.equal(true) + expect(auctionAdUnit1.bids[0].params).to.have.keys('placementId') + expect(auctionAdUnit1.bids[0].size).to.equal('0x0') + expect(auctionAdUnit1.bids[0].crId).to.equal('123456') + expect(auctionAdUnit1.bids[0].ttl).to.equal(bidResponse.ttl) + expect(auctionAdUnit1.bids[0].ttr).to.equal(bidResponse.timeToRespond) + + expect(auctionAdUnit2.auctionId).to.equal(auctionId) + expect(auctionAdUnit2.bids).to.be.an('array') + expect(auctionAdUnit2.bids).to.have.length(2) + + // no bid + expect(auctionAdUnit2.bids[0].bidId).to.equal(bidId2) + expect(auctionAdUnit2.bids[0].bst).to.equal('noBid') + expect(auctionAdUnit2.bids[0].bidder).to.equal('rubicon') + expect(auctionAdUnit2.bids[0].s).to.be.a('number') + expect(auctionAdUnit2.bids[0].e).to.be.a('number') + expect(auctionAdUnit2.bids[0].params).to.have.keys('placementId') + + // timeout + expect(auctionAdUnit2.bids[1].bidId).to.equal(bidId3) + expect(auctionAdUnit2.bids[1].bst).to.equal('bidTimedOut') + expect(auctionAdUnit2.bids[1].bidder).to.equal('ix') + expect(auctionAdUnit2.bids[1].s).to.be.a('number') + expect(auctionAdUnit2.bids[1].e).to.be.a('undefined') + }) + + it('should send "auction" event without all the fields that were set to undefined', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + + simulateAuction() + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody); + + const auctionAdUnit1 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit1)[0] + const auctionAdUnit2 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit2)[0] + + expect(auctionAdUnit1.code).to.equal(undefined) + expect(auctionAdUnit1.transactionId).to.equal(undefined) + expect(auctionAdUnit1.adUnitCode).to.equal(undefined) + expect(auctionAdUnit1.bids[0].auctionStart).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bids).to.equal(undefined) + expect(auctionAdUnit1.bids[0].refererInfo).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidRequestsCount).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidderRequestId).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidderRequestsCount).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidderWinsCount).to.equal(undefined) + expect(auctionAdUnit1.bids[0].schain).to.equal(undefined) + expect(auctionAdUnit1.bids[0].src).to.equal(undefined) + expect(auctionAdUnit1.bids[0].transactionId).to.equal(undefined) + + // no bid + expect(auctionAdUnit2.bids[0].schain).to.equal(undefined) + expect(auctionAdUnit2.bids[0].src).to.equal(undefined) + expect(auctionAdUnit2.bids[0].transactionId).to.equal(undefined) + }) + + it('should mark bid winner and send to the server along with the auction data', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + simulateAuction() + events.emit(constants.EVENTS.BID_WON, bidWon); + clock.tick(1500) + + // no bidWon + expect(server.requests).to.have.length(4) + + const request = JSON.parse(server.requests[3].requestBody); + expect(request.adUnits[0].bids[0].isW).to.equal(true) + }) + + it('should take BID_WON_TIMEOUT from server config if exists', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': {}, + 'BID_WON_TIMEOUT': 500 + })) + + simulateAuction() + events.emit(constants.EVENTS.BID_WON, bidWon); + clock.tick(499) + + // no auction data + expect(server.requests).to.have.length(3) + + clock.tick(1) + + // auction data + expect(server.requests).to.have.length(4) + const request = JSON.parse(server.requests[3].requestBody); + expect(request.adUnits[0].bids[0].isW).to.equal(true) + }) + + it('should send a "bidWon" event to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + simulateAuction() + clock.tick(1500) + events.emit(constants.EVENTS.BID_WON, bidWon); + + expect(server.requests).to.have.length(5) + + const request = JSON.parse(server.requests[4].requestBody); + + expect(request.eventType).to.equal('bidWon') + expect(request.auctionId).to.equal(auctionId) + expect(request.adunid).to.equal(adUnit1) + expect(request.bid.bst).to.equal('bidWon') + expect(request.bid.cur).to.equal('USD') + expect(request.bid.cpm).to.equal(0.5) + }) + + it('should sent adRenderFailed to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + simulateAuction() + clock.tick(1500) + events.emit(constants.EVENTS.AD_RENDER_FAILED, { bidId: 'abcdef', reason: 'exception' }); + + expect(server.requests).to.have.length(5) + + const request = JSON.parse(server.requests[4].requestBody); + + expect(request.eventType).to.equal('adRenderFailed') + expect(request.pvid).to.equal(PAGEVIEW_ID) + expect(request.bidId).to.equal('abcdef') + expect(request.reason).to.equal('exception') + }) + + it('should pick fields according to server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'bidRequested': { + 'sendRaw': 0, + 'pickFields': ['transactionId'] + }, + 'noBid': { + 'sendRaw': 0, + 'pickFields': ['src'] + }, + 'bidResponse': { + 'sendRaw': 0, + 'pickFields': ['adUrl', 'statusMessage'] + }, + 'auctionEnd': { + 'sendRaw': 0, + 'pickFields': ['winningBids'] + }, + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit }); + events.emit(constants.EVENTS.BID_REQUESTED, { ...bidRequested, bids: bidRequested.bids.map(b => { b.transactionId = '123'; return b }) }); + events.emit(constants.EVENTS.NO_BID, { ...noBid, src: 'client' }); + events.emit(constants.EVENTS.BID_RESPONSE, { ...bidResponse, adUrl: '...' }); + events.emit(constants.EVENTS.AUCTION_END, { ...auctionEnd, winningBids: [] }); + events.emit(constants.EVENTS.BID_WON, { ...bidWon, statusMessage: 'msg2' }); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request.adUnits[0].bids[0].transactionId).to.equal('123') + expect(request.adUnits[0].bids[0].adUrl).to.equal('...') + expect(request.adUnits[0].bids[0].statusMessage).to.equal('msg2') + expect(request.adUnits[1].bids[0].src).to.equal('client') + }) + + it('should omit fields according to server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 0, + 'omitFields': ['custom_1'] + }, + 'bidResponse': { + 'sendRaw': 0, + 'omitFields': ['custom_2', 'custom_4', 'custom_5'] + } + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit, custom_1: true }); + events.emit(constants.EVENTS.BID_REQUESTED, { ...bidRequested, bids: bidRequested.bids.map(b => { b.custom_2 = true; return b }) }); + events.emit(constants.EVENTS.NO_BID, { ...noBid, custom_3: true }); + events.emit(constants.EVENTS.BID_RESPONSE, { ...bidResponse, custom_4: true }); + events.emit(constants.EVENTS.AUCTION_END, { ...auctionEnd }); + events.emit(constants.EVENTS.BID_WON, { ...bidWon, custom_5: true }); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request.custom_1).to.equal(undefined) + expect(request.custom_6).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_2).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_4).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_5).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_7).to.equal(undefined) + }) + + it('should omit fields from raw data according to server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 1, + 'omitRawFields': ['custom_1'] + }, + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit, custom_1: true }); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request.eventType).to.equal('auctionInit') + expect(request.custom_1).to.equal(undefined) + }) + + it('should send raw data to custom endpoint if exists in server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 1, + 'endpoint': 'https://pbjs.com' + }, + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit }); + + expect(server.requests[3].url).to.equal('https://pbjs.com') + }) + + it('should send raw events based on server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 0, + }, + 'bidRequested': { + 'sendRaw': 1, + } + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, auctionInit) + events.emit(constants.EVENTS.BID_REQUESTED, bidRequested); + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request).to.deep.equal({ + eventType: 'bidRequested', + pid: 123, + pvid: PAGEVIEW_ID, + pbModuleVersion: '1.0.0', + ...bidRequested + }) + }) + + it('should queue events and raw events until server configuration resolves', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + simulateAuction() + clock.tick(1500) + + expect(server.requests).to.have.length(3) + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 1, + }, + 'bidRequested': { + 'sendRaw': 1, + } + } + })) + + expect(server.requests).to.have.length(5) + expect(JSON.parse(server.requests[3].requestBody).eventType).to.equal('auctionInit') + expect(JSON.parse(server.requests[4].requestBody).eventType).to.equal('bidRequested') + }) + }); + + describe('buildAuctionData', () => { + let auction = { + auctionId, + auctionStart, + auctionEnd, + adUnitCodes: ['mid_1'], + auctionStatus: 'running', + bidderRequests: [], + bidsReceived: [], + noBids: [], + winningBids: [], + timestamp: 1234567, + config: {}, + adUnits: { + mid_1: { + adUnitCode: 'mid_1', + code: 'mid_1', + transactionId: '123dsafasdf', + bids: { + [bidId1]: { + adUnitCode: 'mid_1', + cpm: 0.5 + } + } + } + } + } + + it('should turn adUnits and bids objects into arrays', () => { + const auctionData = buildAuctionData(auction, []) + + expect(auctionData.adUnits).to.be.an('array') + expect(auctionData.adUnits[0].bids).to.be.an('array') + }) + + it('should remove fields from the auction', () => { + const auctionData = buildAuctionData(auction, []) + const auctionFields = Object.keys(auctionData) + const adUnitFields = Object.keys(auctionData.adUnits[0]) + + expect(auctionFields).not.to.contain('adUnitCodes') + expect(auctionFields).not.to.contain('auctionStatus') + expect(auctionFields).not.to.contain('bidderRequests') + expect(auctionFields).not.to.contain('bidsReceived') + expect(auctionFields).not.to.contain('noBids') + expect(auctionFields).not.to.contain('winningBids') + expect(auctionFields).not.to.contain('timestamp') + expect(auctionFields).not.to.contain('config') + + expect(adUnitFields).not.to.contain('adUnitCoe') + expect(adUnitFields).not.to.contain('code') + expect(adUnitFields).not.to.contain('transactionId') + }) + }) + + describe('generatePageViewId', () => { + it('should generate a 19 digits number', () => { + expect(generatePageViewId()).length(19) + }) + }) +}); diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 1cc89240bc3..5e62af9b2fa 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -92,7 +92,7 @@ describe('Parrable ID System', function() { }) it('creates xhr to Parrable that synchronizes the ID', function() { - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK.params); + let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); getIdResult.callback(callbackSpy); @@ -128,7 +128,7 @@ describe('Parrable ID System', function() { let uspString = '1YNN'; uspDataHandler.setConsentData(uspString); parrableIdSubmodule.getId( - P_CONFIG_MOCK.params, + P_CONFIG_MOCK, null, null ).callback(callbackSpy); @@ -138,7 +138,7 @@ describe('Parrable ID System', function() { it('should log an error and continue to callback if ajax request errors', function () { let callBackSpy = sinon.spy(); - let submoduleCallback = parrableIdSubmodule.getId({partner: 'prebid'}).callback; + let submoduleCallback = parrableIdSubmodule.getId({ params: {partner: 'prebid'} }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; expect(request.url).to.contain('h.parrable.com'); @@ -155,7 +155,7 @@ describe('Parrable ID System', function() { describe('response id', function() { it('provides the stored Parrable values if a cookie exists', function() { writeParrableCookie({ eid: P_COOKIE_EID }); - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK.params); + let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); removeParrableCookie(); expect(getIdResult.id).to.deep.equal({ @@ -171,7 +171,7 @@ describe('Parrable ID System', function() { storage.setCookie(oldEidCookieName, oldEid); storage.setCookie(oldOptoutCookieName, 'true'); - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK.params); + let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); expect(getIdResult.id).to.deep.equal({ eid: oldEid, ibaOptout: true @@ -212,9 +212,9 @@ describe('Parrable ID System', function() { }); it('permits an impression when no timezoneFilter is configured', function() { - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', - })).to.have.property('callback'); + } })).to.have.property('callback'); }); it('permits an impression from a blocked timezone when a cookie exists', function() { @@ -224,12 +224,12 @@ describe('Parrable ID System', function() { writeParrableCookie({ eid: P_COOKIE_EID }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } - })).to.have.property('callback'); + } })).to.have.property('callback'); expect(resolvedOptions.called).to.equal(false); removeParrableCookie(); @@ -240,12 +240,12 @@ describe('Parrable ID System', function() { const resolvedOptions = sinon.stub().returns({ timeZone: allowedZone }); Intl.DateTimeFormat.returns({ resolvedOptions }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { allowedZones: [ allowedZone ] } - })).to.have.property('callback'); + } })).to.have.property('callback'); expect(resolvedOptions.called).to.equal(true); }); @@ -254,12 +254,12 @@ describe('Parrable ID System', function() { const resolvedOptions = sinon.stub().returns({ timeZone: 'Iceland' }); Intl.DateTimeFormat.returns({ resolvedOptions }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } - })).to.have.property('callback'); + } })).to.have.property('callback'); expect(resolvedOptions.called).to.equal(true); }); @@ -268,12 +268,12 @@ describe('Parrable ID System', function() { const resolvedOptions = sinon.stub().returns({ timeZone: blockedZone }); Intl.DateTimeFormat.returns({ resolvedOptions }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } - })).to.equal(null); + } })).to.equal(null); expect(resolvedOptions.called).to.equal(true); }); @@ -282,13 +282,13 @@ describe('Parrable ID System', function() { const resolvedOptions = sinon.stub().returns({ timeZone: timezone }); Intl.DateTimeFormat.returns({ resolvedOptions }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { allowedZones: [ timezone ], blockedZones: [ timezone ] } - })).to.equal(null); + } })).to.equal(null); expect(resolvedOptions.called).to.equal(true); }); }); @@ -312,12 +312,12 @@ describe('Parrable ID System', function() { writeParrableCookie({ eid: P_COOKIE_EID }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } - })).to.have.property('callback'); + } })).to.have.property('callback'); removeParrableCookie(); }); @@ -326,12 +326,12 @@ describe('Parrable ID System', function() { const allowedOffset = -5; Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { allowedOffsets: [ allowedOffset ] } - })).to.have.property('callback'); + } })).to.have.property('callback'); expect(Date.prototype.getTimezoneOffset.called).to.equal(true); }); @@ -340,12 +340,12 @@ describe('Parrable ID System', function() { const blockedOffset = 5; Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } - })).to.have.property('callback'); + }})).to.have.property('callback'); expect(Date.prototype.getTimezoneOffset.called).to.equal(true); }); @@ -353,12 +353,12 @@ describe('Parrable ID System', function() { const blockedOffset = -5; Date.prototype.getTimezoneOffset.returns(blockedOffset * 60); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } - })).to.equal(null); + } })).to.equal(null); expect(Date.prototype.getTimezoneOffset.called).to.equal(true); }); @@ -366,13 +366,13 @@ describe('Parrable ID System', function() { const offset = -5; Date.prototype.getTimezoneOffset.returns(offset * 60); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { allowedOffset: [ offset ], blockedOffsets: [ offset ] } - })).to.equal(null); + } })).to.equal(null); expect(Date.prototype.getTimezoneOffset.called).to.equal(true); }); }); diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index d38037f40a1..11de7b13208 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -11,6 +11,12 @@ let events = require('src/events'); let ajax = require('src/ajax'); let utils = require('src/utils'); +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 setUADefault = () => { window.navigator.__defineGetter__('userAgent', function () { return DEFAULT_USER_AGENT }) }; +const setUAMobile = () => { window.navigator.__defineGetter__('userAgent', function () { return MOBILE_USER_AGENT }) }; +const setUANull = () => { window.navigator.__defineGetter__('userAgent', function () { return null }) }; + const { EVENTS: { AUCTION_INIT, @@ -247,6 +253,7 @@ describe('pubmatic analytics adapter', function () { let clock; beforeEach(function () { + setUADefault(); sandbox = sinon.sandbox.create(); xhr = sandbox.useFakeXMLHttpRequest(); @@ -643,6 +650,7 @@ describe('pubmatic analytics adapter', function () { }); it('Logger: currency conversion check', function() { + setUANull(); setConfig({ adServerCurrency: 'JPY', rates: { @@ -695,6 +703,212 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(100); expect(data.s[1].ps[0].ocry).to.equal('JPY'); + expect(data.dvc).to.deep.equal({'plt': 3}); + }); + + it('Logger: regexPattern in bid.params', function() { + setUAMobile(); + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY.bids[1].params.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, BID2); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.dvc).to.deep.equal({'plt': 2}); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); + }); + + it('Logger: regexPattern in bid.bidResponse', function() { + const BID2_COPY = utils.deepClone(BID2); + BID2_COPY.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, BID2_COPY); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, Object.assign({}, BID2_COPY, { + 'status': 'rendered' + })); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.dvc).to.deep.equal({'plt': 1}); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); + }); + + it('Logger: regexPattern in bid.params', function() { + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY.bids[1].params.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, BID2); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); + }); + + it('Logger: regexPattern in bid.bidResponse', function() { + const BID2_COPY = utils.deepClone(BID2); + BID2_COPY.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, BID2_COPY); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, Object.assign({}, BID2_COPY, { + 'status': 'rendered' + })); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); }); }); }); diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index cd4918460db..42818232cda 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -86,6 +86,60 @@ describe('RelaidoAdapter', function () { setUADefault(); }); + it('should return false when missing 300x250 over and 1x1 by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [100, 100], + [300, 100] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + setUADefault(); + }); + + it('should return true when 300x250 by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [300, 250] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + setUADefault(); + }); + + it('should return true when 1x1 by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [1, 1] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + setUADefault(); + }); + + it('should return true when 300x250 over by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [100, 100], + [300, 250] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + setUADefault(); + }); + it('should return false when the uuid are missing', function () { localStorage.removeItem(UUID_KEY); const result = !!(utils.isSafariBrowser()); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index b2adc1f39ff..a38743d634a 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1145,6 +1145,20 @@ describe('the rubicon adapter', function () { }); }); + describe('pubcid support', function () { + it('should send eid_pubcid.org when userIdAsEids contains pubcid', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + pubcid: '1111' + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_pubcid.org']).to.equal('1111^1'); + }); + }); + describe('Criteo support', function () { it('should send eid_criteo.com when userIdAsEids contains criteo', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); @@ -1442,10 +1456,16 @@ describe('the rubicon adapter', function () { expect(post.user.ext.eids[4].source).to.equal('pubcid.org'); expect(post.user.ext.eids[4].uids[0].atype).to.equal(1); expect(post.user.ext.eids[4].uids[0].id).to.equal('4000'); + // example should exist + expect(post.user.ext.eids[5].source).to.equal('example.com'); + expect(post.user.ext.eids[5].uids[0].id).to.equal('333333'); + // id-partner.com + expect(post.user.ext.eids[6].source).to.equal('id-partner.com'); + expect(post.user.ext.eids[6].uids[0].id).to.equal('4444444'); // CriteoId should exist - expect(post.user.ext.eids[5].source).to.equal('criteo.com'); - expect(post.user.ext.eids[5].uids[0].id).to.equal('1111'); - expect(post.user.ext.eids[5].uids[0].atype).to.equal(1); + expect(post.user.ext.eids[7].source).to.equal('criteo.com'); + expect(post.user.ext.eids[7].uids[0].id).to.equal('1111'); + expect(post.user.ext.eids[7].uids[0].atype).to.equal(1); expect(post.regs.ext.gdpr).to.equal(1); expect(post.regs.ext.us_privacy).to.equal('1NYN'); diff --git a/test/spec/modules/sizeMappingV2_spec.js b/test/spec/modules/sizeMappingV2_spec.js index 77e17ef360f..ab06ddc8f51 100644 --- a/test/spec/modules/sizeMappingV2_spec.js +++ b/test/spec/modules/sizeMappingV2_spec.js @@ -205,6 +205,18 @@ describe('sizeMappingV2', function () { expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); }); + it('should filter out adUnit if it does not contain the required property "bids"', function() { + let adUnits = utils.deepClone(AD_UNITS); + delete adUnits[0].mediaTypes; + // before checkAdUnitSetupHook is called, the length of the adUnits should equal '2' + expect(adUnits.length).to.equal(2); + adUnits = checkAdUnitSetupHook(adUnits); + + // after checkAdUnitSetupHook is called, the length of the adUnits should be '1' + expect(adUnits.length).to.equal(1); + expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); + }); + it('should filter out adUnit if it has declared property mediaTypes with an empty object', function () { let adUnits = utils.deepClone(AD_UNITS); adUnits[0].mediaTypes = {}; @@ -657,7 +669,7 @@ describe('sizeMappingV2', function () { }, native: {} }, - bids: [] + bids: [{bidder: 'appnexus', params: 1234}] }]; checkAdUnitSetupHook(adUnit); @@ -679,7 +691,7 @@ describe('sizeMappingV2', function () { }, native: {} }, - bids: [] + bids: [{bidder: 'appnexus', params: 1234}] }]; checkAdUnitSetupHook(adUnit); @@ -698,7 +710,7 @@ describe('sizeMappingV2', function () { mediaTypes: { native: {} }, - bids: [] + bids: [{bidder: 'appnexus', params: 1234}] }]; checkAdUnitSetupHook(adUnit); diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index 2de7cb2c9ff..e3bca240a47 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -425,6 +425,7 @@ describe('Smart bid adapter tests', function () { expect(bid.mediaType).to.equal('video'); expect(bid.vastUrl).to.equal('http://awesome.fake-vast.url'); expect(bid.vastXml).to.equal(''); + expect(bid.content).to.equal(''); expect(bid.width).to.equal(640); expect(bid.height).to.equal(480); expect(bid.creativeId).to.equal('zioeufg'); @@ -473,6 +474,109 @@ describe('Smart bid adapter tests', function () { }); }); + describe('Outstream video tests', function () { + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + }); + + const OUTSTREAM_DEFAULT_PARAMS = [{ + adUnitCode: 'sas_43', + bidId: 'abcd1234', + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[800, 600]] // It seems prebid.js transforms the player size array into an array of array... + } + }, + params: { + siteId: '1234', + pageId: '5678', + formatId: '91', + target: 'test=prebid-outstream', + bidfloor: 0.430, + buId: '7579', + appName: 'Mozilla', + ckId: 43, + video: { + protocol: 7 + } + }, + requestId: 'efgh5679', + transactionId: 'zsfgzzga' + }]; + + var OUTSTREAM_BID_RESPONSE = { + body: { + cpm: 14, + width: 800, + height: 600, + creativeId: 'zioeufga', + currency: 'USD', + isNetCpm: true, + ttl: 300, + adUrl: 'http://awesome.fake-vast2.url', + ad: '', + cSyncUrl: 'http://awesome.fake2.csync.url' + } + }; + + it('Verify outstream video build request', function () { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + } + }); + const request = spec.buildRequests(OUTSTREAM_DEFAULT_PARAMS); + expect(request[0]).to.have.property('url').and.to.equal('https://prg.smartadserver.com/prebid/v1'); + expect(request[0]).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('siteid').and.to.equal('1234'); + expect(requestContent).to.have.property('pageid').and.to.equal('5678'); + expect(requestContent).to.have.property('formatid').and.to.equal('91'); + expect(requestContent).to.have.property('currencyCode').and.to.equal('EUR'); + expect(requestContent).to.have.property('bidfloor').and.to.equal(0.43); + expect(requestContent).to.have.property('targeting').and.to.equal('test=prebid-outstream'); + expect(requestContent).to.have.property('tagId').and.to.equal('sas_43'); + expect(requestContent).to.not.have.property('pageDomain'); + expect(requestContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + expect(requestContent).to.have.property('buid').and.to.equal('7579'); + expect(requestContent).to.have.property('appname').and.to.equal('Mozilla'); + expect(requestContent).to.have.property('ckid').and.to.equal(43); + expect(requestContent).to.have.property('isVideo').and.to.equal(false); + expect(requestContent).to.have.property('videoData'); + expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(7); + expect(requestContent.videoData).to.have.property('playerWidth').and.to.equal(800); + expect(requestContent.videoData).to.have.property('playerHeight').and.to.equal(600); + }); + + it('Verify outstream parse response', function () { + const request = spec.buildRequests(OUTSTREAM_DEFAULT_PARAMS); + const bids = spec.interpretResponse(OUTSTREAM_BID_RESPONSE, request[0]); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(14); + expect(bid.mediaType).to.equal('video'); + expect(bid.vastUrl).to.equal('http://awesome.fake-vast2.url'); + expect(bid.vastXml).to.equal(''); + expect(bid.content).to.equal(''); + expect(bid.width).to.equal(800); + expect(bid.height).to.equal(600); + expect(bid.creativeId).to.equal('zioeufga'); + expect(bid.currency).to.equal('USD'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(300); + expect(bid.requestId).to.equal(OUTSTREAM_DEFAULT_PARAMS[0].bidId); + + expect(function () { + spec.interpretResponse(OUTSTREAM_BID_RESPONSE, { + data: 'invalid Json' + }) + }).to.not.throw(); + }); + }); + describe('External ids tests', function () { it('Verify external ids in request and ids found', function () { config.setConfig({ diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 54ccff870eb..321fed40d83 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -239,8 +239,8 @@ describe('sovrnBidAdapter', function() { expect(data.source.ext.schain.nodes.length).to.equal(1) }); - it('should add the unifiedID if present', function() { - const digitrustRequests = [{ + it('should add ids to the bid request', function() { + const criteoIdRequest = [{ 'bidder': 'sovrn', 'params': { 'tagid': 403370 @@ -254,6 +254,7 @@ describe('sovrnBidAdapter', function() { 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', 'userId': { + 'criteoId': 'A_CRITEO_ID', 'tdid': 'SOMESORTOFID', } }].concat(bidRequests); @@ -263,12 +264,20 @@ describe('sovrnBidAdapter', function() { } }; - const data = JSON.parse(spec.buildRequests(digitrustRequests, bidderRequest).data); - expect(data.user.ext.eids[0].source).to.equal('adserver.org') - expect(data.user.ext.eids[0].uids[0].id).to.equal('SOMESORTOFID') - expect(data.user.ext.eids[0].uids[0].ext.rtiPartner).to.equal('TDID') - }) + const data = JSON.parse(spec.buildRequests(criteoIdRequest, bidderRequest).data); + expect(data.user.ext.eids[0].source).to.equal('criteo.com') + expect(data.user.ext.eids[0].uids[0].id).to.equal('A_CRITEO_ID') + expect(data.user.ext.eids[0].uids[0].atype).to.equal(1) + expect(data.user.ext.eids[1].source).to.equal('adserver.org') + expect(data.user.ext.eids[1].uids[0].id).to.equal('SOMESORTOFID') + expect(data.user.ext.eids[1].uids[0].ext.rtiPartner).to.equal('TDID') + expect(data.user.ext.eids[1].uids[0].atype).to.equal(1) + expect(data.user.ext.tpid[0].source).to.equal('criteo.com') + expect(data.user.ext.tpid[0].uid).to.equal('A_CRITEO_ID') + expect(data.user.ext.prebid_criteoid).to.equal('A_CRITEO_ID') + }); }); + describe('interpretResponse', function () { let response; beforeEach(function () { diff --git a/test/spec/modules/stroeerCoreBidAdapter_spec.js b/test/spec/modules/stroeerCoreBidAdapter_spec.js new file mode 100644 index 00000000000..dd02ebb7c8e --- /dev/null +++ b/test/spec/modules/stroeerCoreBidAdapter_spec.js @@ -0,0 +1,532 @@ +import {assert} from 'chai'; +import {spec} from 'modules/stroeerCoreBidAdapter.js'; +import * as utils from 'src/utils.js'; +import {BANNER, VIDEO} from '../../../src/mediaTypes.js'; + +describe('stroeerCore bid adapter', function () { + let sandbox; + let fakeServer; + let bidderRequest; + let clock; + + beforeEach(() => { + bidderRequest = buildBidderRequest(); + sandbox = sinon.sandbox.create(); + fakeServer = sandbox.useFakeServer(); + clock = sandbox.useFakeTimers(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + function assertStandardFieldsOnBid(bidObject, bidId, ad, width, height, cpm) { + assert.propertyVal(bidObject, 'requestId', bidId); + assert.propertyVal(bidObject, 'ad', ad); + assert.propertyVal(bidObject, 'width', width); + assert.propertyVal(bidObject, 'height', height); + assert.propertyVal(bidObject, 'cpm', cpm); + assert.propertyVal(bidObject, 'currency', 'EUR'); + assert.propertyVal(bidObject, 'netRevenue', true); + assert.propertyVal(bidObject, 'creativeId', ''); + } + + const AUCTION_ID = utils.getUniqueIdentifierStr(); + + // Vendor user ids and associated data + const userIds = Object.freeze({ + criteoId: 'criteo-user-id', + digitrustid: { + data: { + id: 'encrypted-user-id==', + keyv: 4, + privacy: {optout: false}, + producer: 'ABC', + version: 2 + } + }, + lipb: { + lipbid: 'T7JiRRvsRAmh88', + segments: ['999'] + } + }); + + const buildBidderRequest = () => ({ + auctionId: AUCTION_ID, + bidderRequestId: 'bidder-request-id-123', + bidderCode: 'stroeerCore', + timeout: 5000, + auctionStart: 10000, + bids: [{ + bidId: 'bid1', + bidder: 'stroeerCore', + adUnitCode: 'div-1', + mediaTypes: { + banner: { + sizes: [[300, 600], [160, 60]] + } + }, + params: { + sid: 'NDA=' + }, + userId: userIds + }, { + bidId: 'bid2', + bidder: 'stroeerCore', + adUnitCode: 'div-2', + mediaTypes: { + banner: { + sizes: [[728, 90]], + } + }, + params: { + sid: 'ODA=' + }, + userId: userIds + }], + }); + + const buildBidderRequestPreVersion3 = () => { + const request = buildBidderRequest(); + request.bids.forEach((bid) => { + bid.sizes = bid.mediaTypes.banner.sizes; + delete bid.mediaTypes; + bid.mediaType = 'banner'; + }); + return request; + }; + + const buildBidderResponse = () => ({ + 'bids': [{ + 'bidId': 'bid1', 'cpm': 4.0, 'width': 300, 'height': 600, 'ad': '
tag1
', 'tracking': {'brandId': 123} + }, { + 'bidId': 'bid2', 'cpm': 7.3, 'width': 728, 'height': 90, 'ad': '
tag2
' + }] + }); + + const createWindow = (href, params = {}) => { + let {parent, referrer, top, frameElement, placementElements = []} = params; + + const protocol = (href.indexOf('https') === 0) ? 'https:' : 'http:'; + const win = { + frameElement, + parent, + top, + location: { + protocol, href + }, + document: { + createElement: function () { + return { + setAttribute: function () { + } + } + }, + referrer, + getElementById: id => placementElements.find(el => el.id === id) + } + }; + + win.self = win; + + if (!parent) { + win.parent = win; + } + + if (!top) { + win.top = win; + } + + return win; + }; + + function createElement(id, offsetTop = 0) { + return { + id, + getBoundingClientRect: function () { + return { + top: offsetTop, height: 1 + } + } + } + } + + function setupSingleWindow(sandBox, placementElements = [createElement('div-1', 17), createElement('div-2', 54)]) { + const win = createWindow('http://www.xyz.com/', { + parent: win, top: win, frameElement: createElement(undefined, 304), placementElements: placementElements + }); + + win.innerHeight = 200; + + sandBox.stub(utils, 'getWindowSelf').returns(win); + sandBox.stub(utils, 'getWindowTop').returns(win); + + return win; + } + + function setupNestedWindows(sandBox, placementElements = [createElement('div-1', 17), createElement('div-2', 54)]) { + const topWin = createWindow('http://www.abc.org/', {referrer: 'http://www.google.com/?query=monkey'}); + topWin.innerHeight = 800; + + const midWin = createWindow('http://www.abc.org/', {parent: topWin, top: topWin, frameElement: createElement()}); + midWin.innerHeight = 400; + + const win = createWindow('http://www.xyz.com/', { + parent: midWin, top: topWin, frameElement: createElement(undefined, 304), placementElements + }); + + win.innerHeight = 200; + + sandBox.stub(utils, 'getWindowSelf').returns(win); + sandBox.stub(utils, 'getWindowTop').returns(topWin); + + return {topWin, midWin, win}; + } + + it('should only support BANNER mediaType', function () { + assert.deepEqual(spec.supportedMediaTypes, [BANNER]); + }); + + describe('bid validation entry point', () => { + let bidRequest; + + beforeEach(() => { + bidRequest = buildBidderRequest().bids[0]; + }); + + it('should have \"isBidRequestValid\" function', () => { + assert.isFunction(spec.isBidRequestValid); + }); + + it('should pass a valid bid', () => { + assert.isTrue(spec.isBidRequestValid(bidRequest)); + }); + + it('should exclude bids without slot id param', () => { + bidRequest.params.sid = undefined; + assert.isFalse(spec.isBidRequestValid(bidRequest)); + }); + + it('should exclude non-banner bids', () => { + delete bidRequest.mediaTypes.banner; + bidRequest.mediaTypes.video = { + playerSize: [640, 480] + }; + + assert.isFalse(spec.isBidRequestValid(bidRequest)); + }); + + it('should exclude non-banner, pre-version 3 bids', () => { + delete bidRequest.mediaTypes; + bidRequest.mediaType = VIDEO; + assert.isFalse(spec.isBidRequestValid(bidRequest)); + }); + }); + + describe('build request entry point', () => { + it('should have \"buildRequests\" function', () => { + assert.isFunction(spec.buildRequests); + }); + + describe('url on server request info object', () => { + let win; + beforeEach(() => { + win = setupSingleWindow(sandbox); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should use hardcoded url as default endpoint', () => { + const bidReq = buildBidderRequest(); + let serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + assert.equal(serverRequestInfo.method, 'POST'); + assert.isObject(serverRequestInfo.data); + assert.equal(serverRequestInfo.url, 'https://hb.adscale.de/dsh'); + }); + + describe('should use custom url if provided', () => { + const samples = [{ + protocol: 'http:', params: {sid: 'ODA=', host: 'other.com', port: '234', path: '/xyz'}, expected: 'https://other.com:234/xyz' + }, { + protocol: 'https:', params: {sid: 'ODA=', host: 'other.com', port: '234', path: '/xyz'}, expected: 'https://other.com:234/xyz' + }, { + protocol: 'https:', + params: {sid: 'ODA=', host: 'other.com', port: '234', securePort: '871', path: '/xyz'}, + expected: 'https://other.com:871/xyz' + }, { + protocol: 'http:', params: {sid: 'ODA=', port: '234', path: '/xyz'}, expected: 'https://hb.adscale.de:234/xyz' + }, ]; + + samples.forEach(sample => { + it(`should use ${sample.expected} as endpoint when given params ${JSON.stringify(sample.params)} and protocol ${sample.protocol}`, + function () { + win.location.protocol = sample.protocol; + + const bidReq = buildBidderRequest(); + bidReq.bids[0].params = sample.params; + bidReq.bids.length = 1; + + let serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + assert.equal(serverRequestInfo.method, 'POST'); + assert.isObject(serverRequestInfo.data); + assert.equal(serverRequestInfo.url, sample.expected); + }); + }); + }); + }); + + describe('payload on server request info object', () => { + let topWin; + let win; + + let placementElements; + beforeEach(() => { + placementElements = [createElement('div-1', 17), createElement('div-2', 54)]; + ({ topWin, win } = setupNestedWindows(sandbox, placementElements)); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should have expected JSON structure', () => { + clock.tick(13500); + const bidReq = buildBidderRequest(); + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + const expectedTimeout = bidderRequest.timeout - (13500 - bidderRequest.auctionStart); + + assert.equal(expectedTimeout, 1500); + + const expectedJsonPayload = { + 'id': AUCTION_ID, + 'timeout': expectedTimeout, + 'ref': topWin.document.referrer, + 'mpa': true, + 'ssl': false, + 'bids': [{ + 'sid': 'NDA=', 'bid': 'bid1', 'siz': [[300, 600], [160, 60]], 'viz': true + }, { + 'sid': 'ODA=', 'bid': 'bid2', 'siz': [[728, 90]], 'viz': true + }], + 'user': { + 'euids': userIds + } + }; + + // trim away fields with undefined + const actualJsonPayload = JSON.parse(JSON.stringify(serverRequestInfo.data)); + + assert.deepEqual(actualJsonPayload, expectedJsonPayload); + }); + + it('should handle banner sizes for pre version 3', () => { + // Version 3 changes the way how banner sizes are accessed. + // We can support backwards compatibility with version 2.x + const bidReq = buildBidderRequestPreVersion3(); + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + assert.deepEqual(serverRequestInfo.data.bids[0].siz, [[300, 600], [160, 60]]); + assert.deepEqual(serverRequestInfo.data.bids[1].siz, [[728, 90]]); + }); + + describe('optional fields', () => { + it('should skip viz field when unable to determine visibility of placement', () => { + placementElements.length = 0; + const bidReq = buildBidderRequest(); + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + assert.lengthOf(serverRequestInfo.data.bids, 2); + + for (let bid of serverRequestInfo.data.bids) { + assert.isUndefined(bid.viz); + } + }); + + it('should skip ref field when unable to determine document referrer', () => { + // i.e., empty if user came from bookmark, or web page using 'rel="noreferrer" on link, etc + buildBidderRequest(); + + const serverRequestInfo = spec.buildRequests(bidderRequest.bids, bidderRequest); + assert.lengthOf(serverRequestInfo.data.bids, 2); + + for (let bid of serverRequestInfo.data.bids) { + assert.isUndefined(bid.ref); + } + }); + + const gdprSamples = [{consentString: 'RG9ua2V5IEtvbmc=', gdprApplies: true}, {consentString: 'UGluZyBQb25n', gdprApplies: false}]; + gdprSamples.forEach((sample) => { + it(`should add GDPR info ${JSON.stringify(sample)} when provided`, () => { + const bidReq = buildBidderRequest(); + bidReq.gdprConsent = sample; + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + const actualGdpr = serverRequestInfo.data.gdpr; + assert.propertyVal(actualGdpr, 'applies', sample.gdprApplies); + assert.propertyVal(actualGdpr, 'consent', sample.consentString); + }); + }); + + const skippableGdprSamples = [{consentString: null, gdprApplies: true}, // + {consentString: 'UGluZyBQb25n', gdprApplies: null}, // + {consentString: null, gdprApplies: null}, // + {consentString: undefined, gdprApplies: true}, // + {consentString: 'UGluZyBQb25n', gdprApplies: undefined}, // + {consentString: undefined, gdprApplies: undefined}]; + skippableGdprSamples.forEach((sample) => { + it(`should not add GDPR info ${JSON.stringify(sample)} when one or more values are missing`, () => { + const bidReq = buildBidderRequest(); + bidReq.gdprConsent = sample; + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + const actualGdpr = serverRequestInfo.data.gdpr; + assert.isUndefined(actualGdpr); + }); + }); + + it('should be able to build without third party user id data', () => { + const bidReq = buildBidderRequest(); + bidReq.bids.forEach(bid => delete bid.userId); + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + assert.lengthOf(serverRequestInfo.data.bids, 2); + assert.notProperty(serverRequestInfo, 'uids'); + }); + }); + }); + }); + + describe('interpret response entry point', () => { + it('should have \"interpretResponse\" function', () => { + assert.isFunction(spec.interpretResponse); + }); + + const invalidResponses = ['', ' ', ' ', undefined, null]; + invalidResponses.forEach(sample => { + it('should ignore invalid responses (\"' + sample + '\") response', () => { + const result = spec.interpretResponse({body: sample}); + assert.isArray(result); + assert.lengthOf(result, 0); + }); + }); + + it('should interpret a standard response', () => { + const bidderResponse = buildBidderResponse(); + + const result = spec.interpretResponse({body: bidderResponse}); + assertStandardFieldsOnBid(result[0], 'bid1', '
tag1
', 300, 600, 4); + assertStandardFieldsOnBid(result[1], 'bid2', '
tag2
', 728, 90, 7.3); + }); + + it('should return empty array, when response contains no bids', () => { + const result = spec.interpretResponse({body: {bids: []}}); + assert.deepStrictEqual(result, []); + }); + }); + + describe('get user syncs entry point', () => { + let win; + beforeEach(() => { + win = setupSingleWindow(sandbox); + + // fake + win.document.createElement = function () { + const attrs = {}; + return { + setAttribute: (name, value) => { + attrs[name] = value + }, + getAttribute: (name) => attrs[name], + hasAttribute: (name) => attrs[name] !== undefined, + tagName: 'SCRIPT', + } + } + }); + + it('should have \"getUserSyncs\" function', () => { + assert.isFunction(spec.getUserSyncs); + }); + + describe('when iframe option is enabled', () => { + it('should perform user connect when there was a response', () => { + const expectedUrl = 'https://js.adscale.de/pbsync.html'; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, ['']); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + + it('should not perform user connect when there was no response', () => { + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, []); + + assert.deepStrictEqual(userSyncResponse, []); + }); + + describe('and gdpr consent is defined', () => { + describe('and gdpr applies', () => { + it('should place gdpr query param to the user sync url with value of 1', () => { + const expectedUrl = 'https://js.adscale.de/pbsync.html?gdpr=1&gdpr_consent='; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: true}); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + }); + + describe('and gdpr does not apply', () => { + it('should place gdpr query param to the user sync url with zero value', () => { + const expectedUrl = 'https://js.adscale.de/pbsync.html?gdpr=0&gdpr_consent='; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: false}); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + + describe('because consent does not specify it', () => { + it('should place gdpr query param to the user sync url with zero value', () => { + const expectedUrl = 'https://js.adscale.de/pbsync.html?gdpr=0&gdpr_consent='; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {}); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + }); + }); + + describe('and consent string is defined', () => { + it('should pass consent string to gdpr consent query param', () => { + const consentString = 'consent_string'; + const expectedUrl = `https://js.adscale.de/pbsync.html?gdpr=1&gdpr_consent=${consentString}`; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: true, consentString}); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + + it('should correctly escape invalid characters', () => { + const consentString = 'consent ?stri&ng'; + const expectedUrl = `https://js.adscale.de/pbsync.html?gdpr=1&gdpr_consent=consent%20%3Fstri%26ng`; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: true, consentString}); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + }); + }); + }); + + describe('when iframe option is disabled', () => { + it('should not perform user connect even when there was a response', () => { + const userSyncResponse = spec.getUserSyncs({iframeEnabled: false}, ['']); + + assert.deepStrictEqual(userSyncResponse, []); + }); + + it('should not perform user connect when there was no response', () => { + const userSyncResponse = spec.getUserSyncs({iframeEnabled: false}, []); + + assert.deepStrictEqual(userSyncResponse, []); + }); + }); + }); +}); diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index 17841b271d4..a6ae17ec8ff 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -200,8 +200,8 @@ describe('teadsBidAdapter', () => { performance.getEntriesByType('navigation')[0] && performance.getEntriesByType('navigation')[0].responseStart && performance.getEntriesByType('navigation')[0].requestStart && - performance.getEntriesByType('navigation')[0].responseStart >= 0 && - performance.getEntriesByType('navigation')[0].requestStart >= 0 && + performance.getEntriesByType('navigation')[0].responseStart > 0 && + performance.getEntriesByType('navigation')[0].requestStart > 0 && Math.round( performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart ); @@ -211,9 +211,15 @@ describe('teadsBidAdapter', () => { if (ttfbExpectedV2) { expect(payload.timeToFirstByte).to.deep.equal(ttfbExpectedV2.toString()); } else { - const ttfbExpectedV1 = performance.timing.responseStart - performance.timing.requestStart; - - expect(payload.timeToFirstByte).to.deep.equal(ttfbExpectedV1.toString()); + const ttfbWithTimingV1 = performance && + performance.timing.responseStart && + performance.timing.requestStart && + performance.timing.responseStart > 0 && + performance.timing.requestStart > 0 && + performance.timing.responseStart - performance.timing.requestStart; + const ttfbExpectedV1 = ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; + + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpectedV1); } }); diff --git a/test/spec/modules/truereachBidAdapter_spec.js b/test/spec/modules/truereachBidAdapter_spec.js index 5fe053ceb91..36441722648 100644 --- a/test/spec/modules/truereachBidAdapter_spec.js +++ b/test/spec/modules/truereachBidAdapter_spec.js @@ -81,4 +81,25 @@ describe('truereachBidAdapterTests', function () { expect(bid.netRevenue).to.equal(false); expect(bid.meta.advertiserDomains[0]).to.equal('https://www.momagic.com/'); }); + + describe('user_sync', function() { + const user_sync_url = 'http://ads.momagic.com/jsp/usersync.jsp'; + it('register_iframe_pixel_if_iframeEnabled_is_true', function() { + let syncs = spec.getUserSyncs( + {iframeEnabled: true} + ); + expect(syncs).to.be.an('array'); + expect(syncs.length).to.equal(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal(user_sync_url); + }); + + it('if_pixelEnabled_is_true', function() { + let syncs = spec.getUserSyncs( + {pixelEnabled: true} + ); + expect(syncs).to.be.an('array'); + expect(syncs.length).to.equal(0); + }); + }); }); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index c4e52d9a121..c5ab2e249fc 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -20,6 +20,7 @@ import { resetConsentData, setConsentConfig } from 'modules/consentManagement.js'; +import {server} from 'test/mocks/xhr.js'; import {unifiedIdSubmodule} from 'modules/unifiedIdSystem.js'; import {pubCommonIdSubmodule} from 'modules/pubCommonIdSystem.js'; import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; @@ -32,8 +33,8 @@ import {intentIqIdSubmodule} from 'modules/intentIqIdSystem.js'; import {zeotapIdPlusSubmodule} from 'modules/zeotapIdPlusIdSystem.js'; import {sharedIdSubmodule} from 'modules/sharedIdSystem.js'; import {haloIdSubmodule} from 'modules/haloIdSystem.js'; -import {server} from 'test/mocks/xhr.js'; -import {pubProvidedIdSubmodule} from 'modules/pubProvidedSystem.js'; +import {pubProvidedIdSubmodule} from 'modules/pubProvidedIdSystem.js'; +import {criteoIdSubmodule} from 'modules/criteoIdSystem.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -41,22 +42,11 @@ const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; const CONSENT_LOCAL_STORAGE_NAME = '_pbjs_userid_consent_data'; describe('User ID', function () { - function getConfigMock(configArr1, configArr2, configArr3, configArr4, configArr5, configArr6, configArr7, configArr8, configArr9, configArr10) { + function getConfigMock(...configArrays) { return { userSync: { syncDelay: 0, - userIds: [ - (configArr1 && configArr1.length >= 3) ? getStorageMock.apply(null, configArr1) : null, - (configArr2 && configArr2.length >= 3) ? getStorageMock.apply(null, configArr2) : null, - (configArr3 && configArr3.length >= 3) ? getStorageMock.apply(null, configArr3) : null, - (configArr4 && configArr4.length >= 3) ? getStorageMock.apply(null, configArr4) : null, - (configArr5 && configArr5.length >= 3) ? getStorageMock.apply(null, configArr5) : null, - (configArr6 && configArr6.length >= 3) ? getStorageMock.apply(null, configArr6) : null, - (configArr7 && configArr7.length >= 3) ? getStorageMock.apply(null, configArr7) : null, - (configArr8 && configArr8.length >= 3) ? getStorageMock.apply(null, configArr8) : null, - (configArr9 && configArr9.length >= 3) ? getStorageMock.apply(null, configArr9) : null, - (configArr10 && configArr10.length >= 3) ? getStorageMock.apply(null, configArr10) : null - ].filter(i => i) + userIds: configArrays.map(configArray => (configArray && configArray.length >= 3) ? getStorageMock.apply(null, configArray) : null) } } } @@ -94,6 +84,12 @@ describe('User ID', function () { return cfg; } + function findEid(eids, source) { + return eids.find((eid) => { + if (eid.source === source) { return true; } + }); + } + before(function () { coreStorage.setCookie('_pubcid_optout', '', EXPIRED_COOKIE_DATE); localStorage.removeItem('_pbjs_id_optout'); @@ -311,6 +307,108 @@ describe('User ID', function () { expect(typeof (getGlobal()).getUserIdsAsEids).to.equal('function'); expect((getGlobal()).getUserIdsAsEids()).to.deep.equal(createEidsArray((getGlobal()).getUserIds())); }); + + it('pbjs.refreshUserIds refreshes', function() { + let sandbox = sinon.createSandbox(); + + let mockIdCallback = sandbox.stub().returns({id: {'MOCKID': '1111'}}); + + let mockIdSystem = { + name: 'mockId', + decode: function(value) { + return { + 'mid': value['MOCKID'] + }; + }, + getId: mockIdCallback + }; + + setSubmoduleRegistry([mockIdSystem]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [{ + name: 'mockId', + value: {id: {mockId: '1111'}} + }] + } + }); + expect(typeof (getGlobal()).refreshUserIds).to.equal('function'); + + getGlobal().getUserIds(); // force initialization + + // update config so that getId will be called + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [{ + name: 'mockId', + storage: {name: 'mockid', type: 'cookie'}, + }] + } + }); + + getGlobal().refreshUserIds(); + expect(mockIdCallback.callCount).to.equal(1); + }); + + it('pbjs.refreshUserIds refreshes single', function() { + coreStorage.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('REFRESH', '', EXPIRED_COOKIE_DATE); + + let sandbox = sinon.createSandbox(); + let mockIdCallback = sandbox.stub().returns({id: {'MOCKID': '1111'}}); + let refreshUserIdsCallback = sandbox.stub(); + + let mockIdSystem = { + name: 'mockId', + decode: function(value) { + return { + 'mid': value['MOCKID'] + }; + }, + getId: mockIdCallback + }; + + let refreshedIdCallback = sandbox.stub().returns({id: {'REFRESH': '1111'}}); + + let refreshedIdSystem = { + name: 'refreshedId', + decode: function(value) { + return { + 'refresh': value['REFRESH'] + }; + }, + getId: refreshedIdCallback + }; + + setSubmoduleRegistry([refreshedIdSystem, mockIdSystem]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [ + { + name: 'mockId', + storage: {name: 'MOCKID', type: 'cookie'}, + }, + { + name: 'refreshedId', + storage: {name: 'refreshedid', type: 'cookie'}, + } + ] + } + }); + + getGlobal().getUserIds(); // force initialization + + getGlobal().refreshUserIds({submoduleNames: 'refreshedId'}, refreshUserIdsCallback); + + expect(refreshedIdCallback.callCount).to.equal(2); + expect(mockIdCallback.callCount).to.equal(1); + expect(refreshUserIdsCallback.callCount).to.equal(1); + }); }); describe('Opt out', function () { @@ -361,7 +459,7 @@ describe('User ID', function () { }); it('handles config with no usersync object', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({}); // usersync is undefined, and no logInfo message for 'User ID - usersync config updated' @@ -369,14 +467,14 @@ describe('User ID', function () { }); it('handles config with empty usersync object', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({userSync: {}}); expect(typeof utils.logInfo.args[0]).to.equal('undefined'); }); it('handles config with usersync and userIds that are empty objs', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -387,7 +485,7 @@ describe('User ID', function () { }); it('handles config with usersync and userIds with empty names or that dont match a submodule.name', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -404,15 +502,15 @@ describe('User ID', function () { }); it('config with 1 configurations should create 1 submodules', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); 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'); }); - it('config with 11 configurations should result in 12 submodules add', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule]); + it('config with 13 configurations should result in 13 submodules add', function () { + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -450,14 +548,16 @@ describe('User ID', function () { storage: {name: 'haloId', type: 'cookie'} }, { name: 'zeotapIdPlus' + }, { + name: 'criteo' }] } }); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 12 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 13 submodules'); }); it('config syncDelay updates module correctly', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -472,7 +572,7 @@ describe('User ID', function () { }); it('config auctionDelay updates module correctly', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -487,7 +587,7 @@ describe('User ID', function () { }); it('config auctionDelay defaults to 0 if not a number', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -731,6 +831,10 @@ describe('User ID', function () { source: 'pubcid.org', uids: [{id: 'testpubcid', atype: 1}] }); + + // verify no sharedid was added + expect(bid.userId).to.not.have.property('sharedid'); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.be.undefined; }); }); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); @@ -738,6 +842,36 @@ describe('User ID', function () { }, {adUnits}); }); + it('test hook from pubcommonid html5', function (done) { + // simulate existing browser local storage values + localStorage.setItem('pubcid', 'testpubcid'); + localStorage.setItem('pubcid_exp', new Date(Date.now() + 100000).toUTCString()); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'html5'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'pubcid.org', + uids: [{id: 'testpubcid', atype: 1}] + }); + + // verify no sharedid was added + expect(bid.userId).to.not.have.property('sharedid'); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.be.undefined; + }); + }); + localStorage.removeItem('pubcid'); + localStorage.removeItem('pubcid_exp'); + done(); + }, {adUnits}); + }); + it('test hook from pubcommonid config value object', function (done) { setSubmoduleRegistry([pubCommonIdSubmodule]); init(config); @@ -755,7 +889,7 @@ describe('User ID', function () { }, {adUnits}); }); - it('test hook from pubcommonid html5', function (done) { + it('test hook from UnifiedId html5', function (done) { // simulate existing browser local storage values localStorage.setItem('unifiedid_alt', JSON.stringify({'TDID': 'testunifiedid_alt'})); localStorage.setItem('unifiedid_alt_exp', ''); @@ -829,6 +963,29 @@ describe('User ID', function () { }, {adUnits}); }); + it('test hook from criteoIdModule cookie', function (done) { + coreStorage.setCookie('storage_bidid', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 100000).toUTCString())); + + setSubmoduleRegistry([criteoIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['criteo', 'storage_bidid', 'cookie'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.criteoId'); + expect(bid.userId.criteoId).to.equal('test_bidid'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'criteo.com', + uids: [{id: 'test_bidid', atype: 1}] + }); + }); + }); + coreStorage.setCookie('storage_bidid', '', EXPIRED_COOKIE_DATE); + done(); + }, {adUnits}); + }); + it('test hook from liveIntentId html5', function (done) { // simulate existing browser local storage values localStorage.setItem('_li_pbid', JSON.stringify({'unifiedId': 'random-ls-identifier'})); @@ -1310,7 +1467,7 @@ describe('User ID', function () { }, {adUnits}); }); - it('test hook when pubCommonId, unifiedId, id5Id, identityLink, britepoolId, intentIqId, zeotapIdPlus, sharedId, netId and haloId have data to pass', function (done) { + it('test hook when pubCommonId, unifiedId, id5Id, identityLink, britepoolId, intentIqId, zeotapIdPlus, sharedId, netId, haloId and Criteo have data to pass', function (done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'testunifiedid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1324,8 +1481,9 @@ describe('User ID', function () { 'ts': 1590525289611 }), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('storage_criteo', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 5000).toUTCString())); - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], @@ -1336,7 +1494,8 @@ describe('User ID', function () { ['sharedId', 'sharedid', 'cookie'], ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], - ['haloId', 'haloId', 'cookie'])); + ['haloId', 'haloId', 'cookie'], + ['criteo', 'storage_criteo', 'cookie'])); requestBidsHook(function () { adUnits.forEach(unit => { @@ -1373,7 +1532,11 @@ describe('User ID', function () { // also check that haloId id was copied to bid expect(bid).to.have.deep.nested.property('userId.haloId'); expect(bid.userId.haloId).to.equal('testHaloId'); - expect(bid.userIdAsEids.length).to.equal(10); + // also check that criteo id was copied to bid + expect(bid).to.have.deep.nested.property('userId.criteoId'); + expect(bid.userId.criteoId).to.equal('test_bidid'); + + expect(bid.userIdAsEids.length).to.equal(11); }); }); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); @@ -1386,11 +1549,12 @@ describe('User ID', function () { coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('haloId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('storage_criteo', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); - it('test hook when pubCommonId, unifiedId, id5Id, britepoolId, intentIqId, zeotapIdPlus, sharedId, netId and haloId have their modules added before and after init', function (done) { + it('test hook when pubCommonId, unifiedId, id5Id, britepoolId, intentIqId, zeotapIdPlus, sharedId, criteo, netId and haloId have their modules added before and after init', function (done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1404,6 +1568,7 @@ describe('User ID', function () { coreStorage.setCookie('intentIqId', 'testintentIqId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('IDP', btoa(JSON.stringify('zeotapId')), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('storage_criteo', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 5000).toUTCString())); setSubmoduleRegistry([]); @@ -1422,6 +1587,7 @@ describe('User ID', function () { attachIdSystem(intentIqIdSubmodule); attachIdSystem(zeotapIdPlusSubmodule); attachIdSystem(haloIdSubmodule); + attachIdSystem(criteoIdSubmodule); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], @@ -1432,7 +1598,8 @@ describe('User ID', function () { ['sharedId', 'sharedid', 'cookie'], ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], - ['haloId', 'haloId', 'cookie'])); + ['haloId', 'haloId', 'cookie'], + ['criteo', 'storage_criteo', 'cookie'])); requestBidsHook(function () { adUnits.forEach(unit => { @@ -1470,7 +1637,12 @@ describe('User ID', function () { // also check that haloId id data was copied to bid expect(bid).to.have.deep.nested.property('userId.haloId'); expect(bid.userId.haloId).to.equal('testHaloId'); - expect(bid.userIdAsEids.length).to.equal(10); + + // also check that criteo id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.criteoId'); + expect(bid.userId.criteoId).to.equal('test_bidid'); + + expect(bid.userIdAsEids.length).to.equal(11); }); }); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); @@ -1483,6 +1655,7 @@ describe('User ID', function () { coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('haloId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('storage_criteo', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); @@ -1511,6 +1684,192 @@ describe('User ID', function () { }, {adUnits}); }); + it('test sharedid enabled via pubcid cookie', function (done) { + coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('pubcid_sharedid', 'testsharedid', (new Date(Date.now() + 5000).toUTCString())); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + + const customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); + addConfig(customCfg, 'params', {enableSharedId: true}); + config.setConfig(customCfg); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + // verify that the PubCommonId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(findEid(bid.userIdAsEids, 'pubcid.org')).to.deep.equal( + {'source': 'pubcid.org', 'uids': [{'id': 'testpubcid', 'atype': 1}]} + ); + // verify that the sharedid was also copied to bid + expect(bid).to.have.deep.nested.property('userId.sharedid'); + expect(bid.userId.sharedid).to.deep.equal({id: 'testsharedid'}); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.deep.equal( + {'source': 'sharedid.org', 'uids': [{'id': 'testsharedid', 'atype': 1}]} + ); + }); + }); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); + + done(); + }, {adUnits}); + }); + + it('test sharedid disabled via pubcid cookie', function (done) { + coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('pubcid_sharedid', 'testsharedid', (new Date(Date.now() + 5000).toUTCString())); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + + // pubCommonId's support for sharedId is off by default + config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + // verify that the PubCommonId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(findEid(bid.userIdAsEids, 'pubcid.org')).to.deep.equal( + {'source': 'pubcid.org', 'uids': [{'id': 'testpubcid', 'atype': 1}]} + ); + // verify that the sharedid was not added to bid + expect(bid.userId).to.not.have.property('sharedid'); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.be.undefined; + }); + }); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); + + done(); + }, {adUnits}); + }); + + it('test sharedid enabled via pubcid html5', function (done) { + coreStorage.setDataInLocalStorage('pubcid', 'testpubcid'); + coreStorage.setDataInLocalStorage('pubcid_exp', new Date(Date.now() + 5000).toUTCString()); + coreStorage.setDataInLocalStorage('pubcid_sharedid', 'testsharedid'); + coreStorage.setDataInLocalStorage('pubcid_sharedid_exp', new Date(Date.now() + 5000).toUTCString()); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + + const customCfg = getConfigMock(['pubCommonId', 'pubcid', 'html5']); + addConfig(customCfg, 'params', {enableSharedId: true}); + config.setConfig(customCfg); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + // verify that the PubCommonId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(findEid(bid.userIdAsEids, 'pubcid.org')).to.deep.equal( + {'source': 'pubcid.org', 'uids': [{'id': 'testpubcid', 'atype': 1}]} + ); + // verify that the sharedid was also copied to bid + expect(bid).to.have.deep.nested.property('userId.sharedid'); + expect(bid.userId.sharedid).to.deep.equal({id: 'testsharedid'}); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.deep.equal( + {'source': 'sharedid.org', 'uids': [{'id': 'testsharedid', 'atype': 1}]} + ); + }); + }); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); + + coreStorage.removeDataFromLocalStorage('pubcid'); + coreStorage.removeDataFromLocalStorage('pubcid_exp'); + coreStorage.removeDataFromLocalStorage('pubcid_sharedid'); + coreStorage.removeDataFromLocalStorage('pubcid_sharedid_exp'); + done(); + }, {adUnits}); + }); + + it('test expired sharedid via pubcid html5', function (done) { + coreStorage.setDataInLocalStorage('pubcid', 'testpubcid'); + coreStorage.setDataInLocalStorage('pubcid_exp', new Date(Date.now() + 5000).toUTCString()); + + // set sharedid to expired already + coreStorage.setDataInLocalStorage('pubcid_sharedid', 'testsharedid'); + coreStorage.setDataInLocalStorage('pubcid_sharedid_exp', new Date(Date.now() - 100).toUTCString()); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + + const customCfg = getConfigMock(['pubCommonId', 'pubcid', 'html5']); + addConfig(customCfg, 'params', {enableSharedId: true}); + config.setConfig(customCfg); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + // verify that the PubCommonId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(findEid(bid.userIdAsEids, 'pubcid.org')).to.deep.equal( + {'source': 'pubcid.org', 'uids': [{'id': 'testpubcid', 'atype': 1}]} + ); + // verify that the sharedid was not added + expect(bid.userId).to.not.have.property('sharedid'); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.be.undefined; + }); + }); + + coreStorage.removeDataFromLocalStorage('pubcid'); + coreStorage.removeDataFromLocalStorage('pubcid_exp'); + coreStorage.removeDataFromLocalStorage('pubcid_sharedid'); + coreStorage.removeDataFromLocalStorage('pubcid_sharedid_exp'); + done(); + }, {adUnits}); + }); + + it('test pubcid coexisting with sharedid', function (done) { + coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('pubcid_sharedid', 'test111', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': 'test222', + 'ts': 1590525289611 + }), (new Date(Date.now() + 5000).toUTCString())); + + // When both pubcommon and sharedid are configured, pubcommon are invoked first due to + // module loading order. This mean the output from the primary sharedid module will overwrite + // the one in pubcommon. + + setSubmoduleRegistry([pubCommonIdSubmodule, sharedIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], + ['sharedId', 'sharedid', 'cookie'], + )); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + // verify that the PubCommonId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(findEid(bid.userIdAsEids, 'pubcid.org')).to.deep.equal( + {'source': 'pubcid.org', 'uids': [{'id': 'testpubcid', 'atype': 1}]} + ); + // verify that the sharedid was also copied to bid + expect(bid).to.have.deep.nested.property('userId.sharedid'); + expect(bid.userId.sharedid.id).to.equal('test222'); + expect(findEid(bid.userIdAsEids, 'sharedid.org').uids[0].id).to.equal('test222'); + }); + }); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('sharedid', '', EXPIRED_COOKIE_DATE); + + done(); + }, {adUnits}); + }); + it('should add new id system ', function (done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); @@ -1567,7 +1926,7 @@ describe('User ID', function () { 'mid': value['MOCKID'] }; }, - getId: function (params, storedId) { + getId: function (config, storedId) { if (storedId) return {}; return {id: {'MOCKID': '1234'}}; } @@ -1636,6 +1995,7 @@ describe('User ID', function () { sinon.stub(events, 'getEvents').returns([]); sinon.stub(utils, 'triggerPixel'); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('_parrable_eid', '', EXPIRED_COOKIE_DATE); }); @@ -1644,6 +2004,7 @@ describe('User ID', function () { events.getEvents.restore(); utils.triggerPixel.restore(); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('_parrable_eid', '', EXPIRED_COOKIE_DATE); }); @@ -1651,7 +2012,7 @@ describe('User ID', function () { it('pubcid callback with url', function () { let adUnits = [getAdUnitMock()]; let innerAdUnits; - let customCfg = getConfigMock(['pubCommonId', 'pubcid_alt', 'cookie']); + let customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); customCfg = addConfig(customCfg, 'params', {pixelUrl: '/any/pubcid/url'}); setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule]); @@ -1701,6 +2062,85 @@ describe('User ID', function () { events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); expect(server.requests[0].url).to.equal('https://match.adsrvr.org/track/rid?ttd_pid=rubicon&fmt=json'); }); + + it('verify sharedid callback via pubcid when enabled', function () { + let adUnits = [getAdUnitMock()]; + let innerAdUnits; + let customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); + customCfg = addConfig(customCfg, 'params', {pixelUrl: '/any/pubcid/url', enableSharedId: true}); + + server.respondWith('https://id.sharedid.org/id', function(xhr) { + xhr.respond(200, {}, '{"sharedId":"testsharedid"}'); + }); + server.respondImmediately = true; + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + config.setConfig(customCfg); + requestBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}); + + expect(utils.triggerPixel.called).to.be.false; + events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + expect(utils.triggerPixel.getCall(0).args[0]).to.include('/any/pubcid/url'); + + expect(server.requests[0].url).to.equal('https://id.sharedid.org/id'); + expect(coreStorage.getCookie('pubcid_sharedid')).to.equal('testsharedid'); + }); + + it('verify no sharedid callback via pubcid when disabled', function () { + let adUnits = [getAdUnitMock()]; + let innerAdUnits; + let customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); + customCfg = addConfig(customCfg, 'params', {pixelUrl: '/any/pubcid/url'}); + + server.respondWith('https://id.sharedid.org/id', function(xhr) { + xhr.respond(200, {}, '{"sharedId":"testsharedid"}'); + }); + server.respondImmediately = true; + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + config.setConfig(customCfg); + requestBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}); + + expect(utils.triggerPixel.called).to.be.false; + events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + expect(utils.triggerPixel.getCall(0).args[0]).to.include('/any/pubcid/url'); + + expect(server.requests).to.have.lengthOf(0); + expect(coreStorage.getCookie('pubcid_sharedid')).to.be.null; + }); + + it('verify sharedid optout via pubcid when enabled', function () { + let adUnits = [getAdUnitMock()]; + let innerAdUnits; + let customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); + customCfg = addConfig(customCfg, 'params', {pixelUrl: '/any/pubcid/url', enableSharedId: true}); + coreStorage.setCookie('pubcid_sharedid', 'testsharedid', (new Date(Date.now() + 5000).toUTCString())); + + server.respondWith('https://id.sharedid.org/id', function(xhr) { + xhr.respond(200, {}, '{"sharedId":"00000000000000000000000000"}'); + }); + server.respondImmediately = true; + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + config.setConfig(customCfg); + requestBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}); + + expect(utils.triggerPixel.called).to.be.false; + events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + expect(utils.triggerPixel.getCall(0).args[0]).to.include('/any/pubcid/url'); + + expect(server.requests[0].url).to.equal('https://id.sharedid.org/id'); + expect(coreStorage.getCookie('pubcid_sharedid')).to.be.null; + }); }); describe('Set cookie behavior', function () { diff --git a/test/spec/modules/vuukleBidAdapter_spec.js b/test/spec/modules/vuukleBidAdapter_spec.js new file mode 100644 index 00000000000..fdca4085c8e --- /dev/null +++ b/test/spec/modules/vuukleBidAdapter_spec.js @@ -0,0 +1,59 @@ +import { expect } from 'chai'; +import { spec } from 'modules/vuukleBidAdapter.js'; + +describe('vuukleBidAdapterTests', function() { + let bidRequestData = { + bids: [ + { + bidId: 'testbid', + bidder: 'vuukle', + params: { + test: 1 + }, + sizes: [[300, 250]] + } + ] + }; + let request = []; + + it('validate_pub_params', function() { + expect( + spec.isBidRequestValid({ + bidder: 'vuukle', + params: { + test: 1 + } + }) + ).to.equal(true); + }); + + it('validate_generated_params', function() { + request = spec.buildRequests(bidRequestData.bids); + let req_data = request[0].data; + + expect(req_data.bidId).to.equal('testbid'); + }); + + it('validate_response_params', function() { + let serverResponse = { + body: { + 'cpm': 0.01, + 'width': 300, + 'height': 250, + 'creative_id': '12345', + 'ad': 'test ad' + } + }; + + request = spec.buildRequests(bidRequestData.bids); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(0.01); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('12345'); + }); +}); diff --git a/test/spec/modules/zeotapIdPlusIdSystem_spec.js b/test/spec/modules/zeotapIdPlusIdSystem_spec.js index 54082618120..4f9e691f12e 100644 --- a/test/spec/modules/zeotapIdPlusIdSystem_spec.js +++ b/test/spec/modules/zeotapIdPlusIdSystem_spec.js @@ -34,26 +34,23 @@ function getAdUnitMock(code = 'adUnit-code') { }; } -describe('Zeotap ID System', function() { - let getDataFromLocalStorageStub, localStorageIsEnabledStub; - let getCookieStub, cookiesAreEnabledStub; - beforeEach(function () { - getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); - localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); - getCookieStub = sinon.stub(storage, 'getCookie'); - cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); - }); +function unsetCookie() { + storage.setCookie(ZEOTAP_COOKIE_NAME, ''); +} - afterEach(function () { - getDataFromLocalStorageStub.restore(); - localStorageIsEnabledStub.restore(); - getCookieStub.restore(); - cookiesAreEnabledStub.restore(); - }); +function unsetLocalStorage() { + storage.setDataInLocalStorage(ZEOTAP_COOKIE_NAME, ''); +} +describe('Zeotap ID System', function() { describe('test method: getId', function() { + afterEach(() => { + unsetCookie(); + unsetLocalStorage(); + }); + it('provides the stored Zeotap id if a cookie exists', function() { - getCookieStub.withArgs(ZEOTAP_COOKIE_NAME).returns(ENCODED_ZEOTAP_COOKIE); + storage.setCookie(ZEOTAP_COOKIE_NAME, ENCODED_ZEOTAP_COOKIE); let id = zeotapIdPlusSubmodule.getId(); expect(id).to.deep.equal({ id: ENCODED_ZEOTAP_COOKIE @@ -61,7 +58,7 @@ describe('Zeotap ID System', function() { }); it('provides the stored Zeotap id if cookie is absent but present in local storage', function() { - getDataFromLocalStorageStub.withArgs(ZEOTAP_COOKIE_NAME).returns(ENCODED_ZEOTAP_COOKIE); + storage.setDataInLocalStorage(ZEOTAP_COOKIE_NAME, ENCODED_ZEOTAP_COOKIE); let id = zeotapIdPlusSubmodule.getId(); expect(id).to.deep.equal({ id: ENCODED_ZEOTAP_COOKIE @@ -99,10 +96,18 @@ describe('Zeotap ID System', function() { beforeEach(function() { adUnits = [getAdUnitMock()]; + storage.setCookie( + ZEOTAP_COOKIE_NAME, + ENCODED_ZEOTAP_COOKIE + ); setSubmoduleRegistry([zeotapIdPlusSubmodule]); init(config); config.setConfig(getConfigMock()); - getCookieStub.withArgs(ZEOTAP_COOKIE_NAME).returns(ENCODED_ZEOTAP_COOKIE); + }); + + afterEach(function() { + unsetCookie(); + unsetLocalStorage(); }); it('when a stored Zeotap ID exists it is added to bids', function(done) { diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 960ccf08c92..afad1d665fd 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1197,6 +1197,14 @@ describe('Unit: Prebid Module', function () { assert.deepEqual($$PREBID_GLOBAL$$.getAllWinningBids()[0], adResponse); }); + it('should replace ${CLICKTHROUGH} macro in winning bids response', function () { + pushBidResponseToAuction({ + ad: "" + }); + $$PREBID_GLOBAL$$.renderAd(doc, bidId, {clickThrough: 'https://someadserverclickurl.com'}); + expect(adResponse).to.have.property('ad').and.to.match(/https:\/\/someadserverclickurl\.com/i); + }); + it('fires billing url if present on s2s bid', function () { const burl = 'http://www.example.com/burl'; pushBidResponseToAuction({ @@ -1765,6 +1773,32 @@ describe('Unit: Prebid Module', function () { expect(auctionArgs.adUnits[0].mediaTypes.native.icon).to.exist; assert.ok(logErrorSpy.calledWith('Please use an array of sizes for native.icon.sizes field. Removing invalid mediaTypes.native.icon.sizes property from request.')); }); + + it('should throw error message and remove adUnit if adUnit.bids is not defined correctly', function () { + const adUnits = [{ + code: 'ad-unit-1', + mediaTypes: { + banner: { + sizes: [300, 400] + } + }, + bids: [{code: 'appnexus', params: 1234}] + }, { + code: 'bad-ad-unit-2', + mediaTypes: { + banner: { + sizes: [300, 400] + } + } + }]; + + $$PREBID_GLOBAL$$.requestBids({ + adUnits: adUnits + }); + expect(auctionArgs.adUnits.length).to.equal(1); + expect(auctionArgs.adUnits[1]).to.not.exist; + assert.ok(logErrorSpy.calledWith("Detected adUnit.code 'bad-ad-unit-2' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.")); + }); }); }); });